char-slop/city-role-bot
discord auto role applicator bot for charlotte polycule city
git clone https://git.t4t.associates/char-slop/city-role-bot
d72003f
main
1import { MessageFlags , type EditInteractionContent } from 'oceanic.js' 2import { deletePanel , panels , setPanel } from '../store.ts' 3import { normalizeEmoji } from '../emoji.ts' 4import { guard , slash , slashSub } from '../rosepack.ts' 5 6const ephemeral = ( content :string ) :EditInteractionContent => ({ 7 content, 8flags :MessageFlags . EPHEMERAL 9}) 10 11const ROLE_INPUT = / ^(?:<@&)?(\d+)>?$ / 12const MESSAGE_INPUT = / ^(?:https:\/\/discord(?:app)?\.com\/channels\/\d+\/\d+\/)?(\d+)$ / 13 14// declared on the leaves rather than the root so the leaf contexts get 15// guild-narrowed interaction types 16const adminOnly = [ 17guard . guild ({ message :'this command only works in a server.' }), 18guard . userPermissions ( 'MANAGE_ROLES' , { message :'you need manage roles to do that.' }) 19] 20 21export default slash ({ 22name :'reactrole' , 23description :'manage reaction-role panels' , 24subcommands :{ 25create :slashSub ({ 26description :'post a new reaction-role message in this channel' , 27guards :adminOnly , 28options :{ 29message :{ 30description :'what the panel message should say' , 31kind :'string' , 32maxLength :2_000 , 33required :true 34}, 35emoji :{ 36description :'the emoji users react with' , 37kind :'string' , 38required :true 39}, 40role :{ 41description :'the role granted by reacting (mention or id)' , 42kind :'string' , 43required :true 44} 45}, 46 47async execute ( context ) { 48const { client} = context 49const guildID = context . interaction . guildID 50const channelID = context . interaction . channelID 51 52const emoji = normalizeEmoji ( context . options . emoji ) 53if ( emoji === undefined ) { 54await context . reply ( ephemeral ( 'that is not a valid emoji. use a single unicode or custom emoji.' )) 55return 56} 57 58const roleID = ROLE_INPUT . exec ( context . options . role . trim ())?.[ 1 ] 59if ( roleID === undefined ) { 60await context . reply ( ephemeral ( 'pass the role as a mention or an id.' )) 61return 62} 63 64const guild = client . guilds . get ( guildID ) 65const role = 66guild ?. roles . get ( roleID ) ?? 67( await client . rest . guilds . getRole ( guildID , roleID ). catch (() => undefined )) 68if ( role === undefined ) { 69await context . reply ( ephemeral ( 'that role does not exist in this server.' )) 70return 71} 72 73// startup reconciliation revokes the role from anyone who has not 74// reacted, so two panels sharing one role would fight each other 75for ( const [ existingID , existing ] of panels ) { 76if ( existing . guildID === guildID && existing . roleID === roleID ) { 77await context . reply ( 78ephemeral ( 79`that role already has a panel: https://discord.com/channels/ ${ guildID } / ${ existing . channelID } / ${ existingID } ` 80) 81) 82return 83} 84} 85 86if ( guild !== undefined ) { 87const botMember = guild . members . get ( client . user . id ) 88if ( botMember !== undefined ) { 89const top = Math . max ( 0 , ...botMember . roles . map (( id ) => guild . roles . get ( id )?. position ?? 0 )) 90if ( role . position >= top ) { 91await context . reply ( 92ephemeral ( 'the target role must sit below my highest role before i can grant it.' ) 93) 94return 95} 96} 97} 98 99let message 100try { 101message = await client . rest . channels . createMessage ( channelID , { 102content :context . options . message 103}) 104} catch { 105await context . reply ( 106ephemeral ( 107'i could not post the panel here. check that i have view channel and send messages in this channel.' 108) 109) 110return 111} 112try { 113await client . rest . channels . createReaction ( channelID , message . id , emoji ) 114} catch { 115await client . rest . channels . deleteMessage ( channelID , message . id ). catch (() => {}) 116await context . reply ( 117ephemeral ( 118'i could not react to the new message. check that i have add reactions and read message history here.' 119) 120) 121return 122} 123 124setPanel ( message . id , { channelID, emoji, guildID, reactors :new Set (), roleID}) 125await context . reply ( 126ephemeral ( `panel created: https://discord.com/channels/ ${ guildID } / ${ channelID } / ${ message . id } ` ) 127) 128} 129}), 130 131remove :slashSub ({ 132description :'stop a reaction-role message from granting roles' , 133guards :adminOnly , 134options :{ 135message :{ 136description :'the panel message, as an id or link' , 137kind :'string' , 138required :true 139} 140}, 141 142async execute ( context ) { 143const messageID = MESSAGE_INPUT . exec ( context . options . message . trim ())?.[ 1 ] 144if ( messageID === undefined ) { 145await context . reply ( ephemeral ( 'pass the panel message as an id or a message link.' )) 146return 147} 148await context . reply ( 149ephemeral ( 150deletePanel ( messageID ) 151 ?'that message no longer grants roles.' 152 :'there is no panel registered for that message.' 153) 154) 155} 156}) 157} 158})