@@ -9,13 +9,23 @@ const yargs = require('yargs');
99const { fetch } = require ( 'undici' ) ;
1010const ipcMessageHandler = require ( './discord/ipcMessage' ) ;
1111const { banner, setDefaultFont } = require ( './discord/figletBanners' ) ;
12- const { PREFIX , replaceIrcEscapes, PrivmsgMappings, NetworkNotMatchedError, UserCommandNotFound, scopedRedisClient } = require ( './util' ) ;
1312const userCommands = require ( './discord/userCommands' ) ;
1413const { formatKVs, aliveKey, ticklePmChanExpiry } = require ( './discord/common' ) ;
1514const { plotMpmData } = require ( './discord/plotting' ) ;
1615const eventHandlers = require ( './discord/events' ) ;
1716const registerContextMenus = require ( './discord/contextMenus' ) ;
1817const parsers = require ( './lib/parsers' ) ;
18+ const UCHistory = require ( './discord/userCommandHistory' ) ;
19+ const {
20+ PREFIX ,
21+ replaceIrcEscapes,
22+ PrivmsgMappings,
23+ NetworkNotMatchedError,
24+ AmbiguousMatchResultError,
25+ UserCommandNotFound,
26+ scopedRedisClient,
27+ fmtDuration
28+ } = require ( './util' ) ;
1929
2030require ( './logger' ) ( 'discord' ) ;
2131
@@ -198,7 +208,11 @@ async function alivenessCheck () {
198208 delete pendingAliveChecks [ nick ] ;
199209 } , 30 * 1000 ) ;
200210
201- await msg ( ctx , network , nick , ...messageComps ) ;
211+ try {
212+ await msg ( ctx , network , nick , ...messageComps ) ;
213+ } catch ( e ) {
214+ console . error ( `Aliveness check for ${ network } /${ nick } threw!` , e ) ;
215+ }
202216 }
203217 }
204218
@@ -366,36 +380,41 @@ client.once('ready', async () => {
366380
367381 siteCheck ( ) ;
368382
383+ const getDiscordChannelById = ( id ) => client . channels . cache . get ( id ) ;
369384 let allowedSpeakerCommandHandler = ( ) => {
370385 throw new Error ( 'allowedSpeakerCommandHandler not initialized! not configured?' ) ;
371386 } ;
372387
373388 if ( config . app . allowedSpeakers . length ) {
374- allowedSpeakerCommandHandler = async ( data , toChanId ) => {
389+ allowedSpeakerCommandHandler = async ( data , toChanId , {
390+ autoPrefixCurrentCommandChar = false
391+ } = { } ) => {
375392 const trimContent = data . content . replace ( / ^ \s + / , '' ) ;
376- if ( trimContent [ 0 ] !== '!' ) {
393+
394+ if ( ! autoPrefixCurrentCommandChar && trimContent [ 0 ] !== config . app . allowedSpeakersCommandPrefixCharacter ) {
377395 return ;
378396 }
379397
380398 const pipedHandler = parsers . parseMessageStringForPipes ( trimContent , ( content ) =>
381- allowedSpeakerCommandHandler ( Object . assign ( { } , data , { content } ) , toChanId ) ) ;
399+ allowedSpeakerCommandHandler ( Object . assign ( { } , data , { content } ) , toChanId , { autoPrefixCurrentCommandChar } ) ) ;
382400
383401 if ( pipedHandler ) {
384402 return pipedHandler ( ) ;
385403 }
386404
387- let { command, args } = parsers . parseCommandAndArgs ( trimContent ) ;
405+ let { command, args } = parsers . parseCommandAndArgs ( trimContent , { autoPrefixCurrentCommandChar } ) ;
406+ console . log ( trimContent , '-> user command parsed ->' , { command, args } ) ;
388407 args = parsers . parseArgsForQuotes ( args ) ;
408+ console . debug ( 'args parsed for quotes' , args ) ;
389409
390410 const fmtedCmdStr = '`' + `${ command } ${ args . join ( ' ' ) } ` + '`' ;
391- console . log ( trimContent , 'USER CMD PARSED' , command , fmtedCmdStr , args ) ;
392411 const redis = new Redis ( config . redis . url ) ;
393412
394413 const zEvents = [ ] ;
395414 let resolvedName ;
396415 try {
397416 const cmdFunc = userCommands ( command ) ;
398- resolvedName = cmdFunc ?. __resolvedFullCommandName ;
417+ resolvedName = cmdFunc ?. __resolvedFullCommandName ?? command ;
399418
400419 // this should be removed ASAP in favor of scopedRedisClient,
401420 // but need to find all uses of it first...
@@ -444,7 +463,7 @@ client.once('ready', async () => {
444463 throw new UserCommandNotFound ( ) ;
445464 }
446465
447- const result = await cmdFunc ( {
466+ const context = {
448467 stats,
449468 redis,
450469 publish, // TODO: replace all uses of `redis` with `publish` (and others if needed)
@@ -462,11 +481,18 @@ client.once('ready', async () => {
462481 channelsById,
463482 categoriesByName,
464483 toChanId,
465- getDiscordChannelById : ( id ) => client . channels . cache . get ( id ) ,
484+ getDiscordChannelById,
466485 discordMessage : data
467- } , ...args ) ;
486+ } ;
487+
488+ if ( argObj . help || argObj . h ) {
489+ context . options . _ = [ resolvedName ] ;
490+ zEvents . push ( 'commandSuccess' ) ;
491+ return userCommands ( 'help' ) ( context ) ;
492+ }
468493
469- console . log ( `Exec'ed user command ${ command } with args [${ args . join ( ', ' ) } ]` , argObj , '-->' , result ) ;
494+ const result = await cmdFunc ( context , ...args ) ;
495+ console . log ( `Executed user command "${ command } " (${ resolvedName } ) with result -->\n` , result ) ;
470496
471497 let toBotChan ;
472498 if ( result && result . __drcFormatter ) {
@@ -494,19 +520,40 @@ client.once('ready', async () => {
494520 if ( ucErr instanceof NetworkNotMatchedError ) {
495521 sendToBotChan ( `Unable to find a matching network for "${ ucErr . message } "` ) ;
496522 zEvents . push ( 'netNotMatched' ) ;
523+ } else if ( ucErr instanceof AmbiguousMatchResultError ) {
524+ sendToBotChan ( {
525+ embeds : [
526+ new MessageEmbed ( )
527+ . setTitle ( `Ambiguous command name "\`${ fmtedCmdStr . trim ( ) } \`"` )
528+ . setColor ( 'RED' )
529+ . setDescription ( ucErr . message )
530+ ]
531+ } , true ) ;
497532 } else {
498533 sendToBotChan ( fmtedCmdStr + `threw an error! (${ ucErr . name } ):` +
499534 ' `' + ucErr . message + '`' ) ;
500535 zEvents . push ( 'otherError' ) ;
501536 }
502537 } finally {
503538 redis . disconnect ( ) ;
539+
540+ const chan = await getDiscordChannelById ( data . channelId ) ;
541+ const { hashKey } = await UCHistory . push ( trimContent , {
542+ zEvents,
543+ sentBy : data . author ?. tag ?? '<system>' ,
544+ sentIn : {
545+ channel : chan ?. name ?? '<system>' ,
546+ guild : chan ?. guild ?. name ?? '<system>'
547+ }
548+ } ) ;
549+
504550 scopedRedisClient ( async ( client , prefix ) => {
505551 const keyArr = [ prefix , 'userCommandCalls' ] ;
506552 const zNewCounts = await Promise . all (
507553 zEvents . map ( ( evName ) => client . zincrby ( [ ...keyArr , evName ] . join ( ':' ) , '1' , resolvedName ?? command ) )
508554 ) ;
509- console . log ( 'Logging UC zEvents!' , command , 'is' , resolvedName , '||' , zEvents , '->>' , zNewCounts ) ;
555+ console . log ( `User command logged as ${ hashKey } :` ,
556+ command , 'is' , resolvedName , '/ events:' , zEvents , '->>' , zNewCounts ) ;
510557 } ) ;
511558 }
512559 } ;
@@ -549,13 +596,29 @@ client.once('ready', async () => {
549596 const uCfg = await scopedRedisClient ( async ( redis ) => userCommands ( 'config' ) ( { redis } , 'load' ) ) ;
550597
551598 if ( uCfg . error ) {
552- sendToBotChan ( `\nReloading user configuration failed:\n\n**${ uCfg . error . message } **\n` ) ;
553- sendToBotChan ( '\nUsing default user configuration:\n\n' + formatKVs ( config . user ) ) ;
599+ sendToBotChan ( {
600+ embeds : [
601+ new MessageEmbed ( )
602+ . setTitle ( 'Reloading configuration failed: using defaults!' )
603+ . setDescription ( formatKVs ( config . user ) ) . setColor ( 'RED' )
604+ ]
605+ } , true ) ;
554606 console . warn ( 'Reloading user config failed' , uCfg . error ) ;
555607 } else {
556- sendToBotChan ( '\nUser configuration:\n\n' + formatKVs ( uCfg ) ) ;
608+ sendToBotChan ( {
609+ embeds : [
610+ new MessageEmbed ( ) . setTitle ( 'User configuration' ) . setDescription ( formatKVs ( uCfg ) ) . setColor ( 'AQUA' )
611+ ]
612+ } , true ) ;
557613 }
558614
615+ sendToBotChan ( {
616+ embeds : [
617+ new MessageEmbed ( ) . setTitle ( config . app . allowedSpeakersCommandPrefixCharacter )
618+ . setDescription ( 'is the user command prefix character.' ) . setColor ( 'ORANGE' )
619+ ]
620+ } , true ) ;
621+
559622 console . log ( 'Discovered private messaging category:' , config . discord . privMsgCategoryId , channelsById [ config . discord . privMsgCategoryId ] ) ;
560623 if ( ! config . discord . privMsgCategoryId || ! channelsById [ config . discord . privMsgCategoryId ] ) {
561624 const potentials = Object . keys ( categoriesByName ) . filter ( x => x . match ( / p r i v (?: a t e ) ? \s * m e ? s (?: s a ) ? g e ? s ? / ig) || x === 'PMs' ) ;
@@ -592,10 +655,25 @@ client.once('ready', async () => {
592655 const realDel = await scopedRedisClient ( async ( aliveClient ) => {
593656 const realDel = [ ] ;
594657 for ( const [ chanId , o ] of toDel ) {
595- const network = PrivmsgMappings . findNetworkForKey ( chanId ) ;
658+ const network = await PrivmsgMappings . findNetworkForKey ( chanId ) ;
659+ if ( ! network ) {
660+ console . warn ( `No network found for PM channel ${ chanId } ! Removing.` ) ;
661+ realDel . push ( [ chanId , o ] ) ;
662+ continue ;
663+ }
664+
665+ const aKey = aliveKey ( network , chanId ) ;
666+ const pmChanTTL = await aliveClient . ttl ( aKey ) ;
667+ if ( pmChanTTL === - 1 ) {
668+ continue ;
669+ }
670+
596671 // only delete channels that have expired in Redis (assuming here that we've missed the keyspace notification for some reason)
597- if ( ! ( await aliveClient . get ( aliveKey ( network , chanId ) ) ) ) {
672+ if ( ! ( await aliveClient . get ( aKey ) ) ) {
598673 realDel . push ( [ chanId , o ] ) ;
674+ } else {
675+ const chanObj = await PrivmsgMappings . get ( network , chanId ) ;
676+ console . info ( `PM channel for ${ chanObj . target } on ${ network } (${ chanId } ) still has ${ fmtDuration ( 0 , true , pmChanTTL * 1000 ) } to live.` ) ;
599677 }
600678 }
601679 return realDel ;
@@ -618,9 +696,14 @@ client.once('ready', async () => {
618696
619697 const privMsgExpiryListener = new Redis ( config . redis . url ) ;
620698 privMsgExpiryListener . on ( 'pmessage' , async ( _chan , key , event ) => {
621- console . log ( 'EXPIRY MSG ' , key , event ) ;
699+ console . log ( 'Expiry message ' , key , event ) ;
622700 const [ , prefix , type , trackingType , id , network ] = key . split ( ':' ) ;
623701
702+ if ( ! ( await client . channels . cache . get ( id ) ) ) {
703+ console . error ( `Expiry message for unknown channel ID ${ id } !` , prefix , type , trackingType , network ) ;
704+ return ;
705+ }
706+
624707 if ( prefix !== PREFIX ) {
625708 stats . errors ++ ;
626709 console . error ( `bad prefix for keyspace notification! ${ prefix } ` , key , event ) ;
@@ -631,9 +714,9 @@ client.once('ready', async () => {
631714 if ( type === 'pmchan' ) {
632715 if ( trackingType === 'aliveness' ) {
633716 console . log ( `PM channel ${ id } :${ network } expired! Removing...` ) ;
634- const chInfo = Object . entries ( PrivmsgMappings . forNetwork ( network ) ) . find ( ( [ chId ] ) => chId == id ) ?. [ 1 ] ; // eslint-disable-line eqeqeq
717+ const chInfo = Object . entries ( await PrivmsgMappings . forNetwork ( network ) ) . find ( ( [ chId ] ) => chId == id ) ?. [ 1 ] ; // eslint-disable-line eqeqeq
635718 if ( ! chInfo || ! chInfo . target || ! channelsById [ id ] ) {
636- console . error ( 'bad chinfo?!' , key , event , chInfo , channelsById [ id ] , PrivmsgMappings . forNetwork ( network ) ) ;
719+ console . error ( 'bad chinfo?!' , key , event , chInfo , channelsById [ id ] , await PrivmsgMappings . forNetwork ( network ) ) ;
637720 return ;
638721 }
639722
@@ -643,13 +726,14 @@ client.once('ready', async () => {
643726 }
644727
645728 const toTime = Number ( new Date ( ) ) ;
646- const queryArgs = [ network , 'get' , chInfo . target , `--from=${ chInfo . created } ` , `--to=${ toTime } ` ] ;
729+ const queryArgs = [ network , chInfo . target , `--from=${ chInfo . created } ` , `--to=${ toTime } ` , '--everything' ] ;
647730
648731 const rmEmbed = new MessageEmbed ( )
649732 . setColor ( config . app . stats . embedColors . irc . privMsg )
650733 . setTitle ( 'Private Message channel cleanup' )
651734 . setDescription ( 'I removed the following channel due to inactivity:' )
652- . addField ( channelsById [ id ] . name , 'Query logs for this session with:\n`' + `!logs ${ queryArgs . join ( ' ' ) } ` + '`' ) ;
735+ . addField ( channelsById [ id ] . name , 'Query logs for this session with:\n`' +
736+ config . app . allowedSpeakersCommandPrefixCharacter + `logs ${ queryArgs . join ( ' ' ) } ` + '`' ) ;
653737
654738 const buttonId = [ id , crypto . randomBytes ( 8 ) . toString ( 'hex' ) ] . join ( '-' ) ;
655739 const actRow = new MessageActionRow ( )
@@ -664,27 +748,30 @@ client.once('ready', async () => {
664748 sendToBotChan,
665749 channelsById,
666750 network,
751+ categoriesByName,
667752 publish : eventHandlerContext . publish ,
668753 argObj : {
669754 _ : queryArgs
670755 } ,
671756 options : {
672757 from : chInfo . created ,
673- to : toTime
758+ to : toTime ,
759+ everything : true ,
760+ _ : queryArgs . slice ( 0 , 2 )
674761 }
675762 } , ...queryArgs ) ;
676763
677764 interaction . update ( { embeds : [ rmEmbed ] , components : [ ] } ) ;
678765 sendToBotChan ( logs ) ;
679766 } ) ;
680767
681- sendToBotChan ( `:arrow_down: :rotating_light: :mega: ${ allowedSpeakersMentionString ( [ '' , '' ] ) } ` ) ;
682768 client . channels . cache . get ( config . irc . quitMsgChanId ) . send ( {
683769 embeds : [ new MessageEmbed ( )
684770 . setColor ( config . app . stats . embedColors . irc . privMsg )
685771 . setTitle ( 'Private Message channel cleanup' )
686772 . setDescription ( 'I removed the following channel due to inactivity:' )
687- . addField ( channelsById [ id ] . name , 'Query logs for this session with the button below or:\n`' + `!logs ${ queryArgs . join ( ' ' ) } ` + '`' ) ] ,
773+ . addField ( channelsById [ id ] . name , 'Query logs for this session with the button below or:\n`' +
774+ config . app . allowedSpeakersCommandPrefixCharacter + `logs ${ queryArgs . join ( ' ' ) } ` + '`' ) ] ,
688775 components : [ actRow ]
689776 } ) ;
690777
@@ -722,7 +809,7 @@ client.once('ready', async () => {
722809 } ) ;
723810 } catch ( err ) {
724811 stats . errors ++ ;
725- console . error ( ' removalWarning KS notification threw' , err ) ;
812+ console . error ( ` removalWarning KS notification threw (chan ${ id } )` , err ) ;
726813 }
727814 }
728815 }
@@ -785,7 +872,7 @@ client.once('ready', async () => {
785872
786873 for ( const connectCmd of onConnectCmds ) {
787874 console . log ( await sendToBotChan ( `Running connect command for \`${ network } \`: \`${ connectCmd } \`` ) ) ;
788- await allowedSpeakerCommandHandler ( { content : connectCmd } ) ;
875+ await allowedSpeakerCommandHandler ( { content : connectCmd } , null , { autoPrefixCurrentCommandChar : true } ) ;
789876 }
790877 }
791878 } ) ;
@@ -821,7 +908,7 @@ client.once('ready', async () => {
821908 console . log ( 'Re-connected!' ) ;
822909 }
823910
824- const HB_FUDGE_FACTOR = 1.01 ;
911+ const HB_FUDGE_FACTOR = 1.05 ;
825912 let ircLastHeartbeat = Number ( new Date ( ) ) ;
826913 ircHeartbeatListener = new Redis ( config . redis . url ) ;
827914 ircHeartbeatListener . on ( 'message' , ( ...a ) => {
@@ -902,7 +989,6 @@ client.once('ready', async () => {
902989 } ) ) ;
903990
904991 console . log ( 'Waiting for irc:ready...' ) ;
905- sendToBotChan ( 'Waiting for IRC bridge...' ) ;
906992 const readyRes = await ircReady . promise ;
907993 _isReconnect = readyRes ?. isReconnect ;
908994
0 commit comments