import { httpGET } from '@squeebot/core/lib/common'; import { logger } from '@squeebot/core/lib/core'; import { Plugin, Configurable, EventListener, DependencyLoad, } from '@squeebot/core/lib/plugin'; import { IMessage, MessageResolver, ProtocolFeatureFlag, } from '@squeebot/core/lib/types'; interface IApiResponse { partOfSpeech: string; text: string; word: string; } interface IDefinition { partOfSpeech: string; text: string; } interface IWordCache { lastIndex: { name: string; index: number; lastTime: number; }[]; definitions: IDefinition[]; inserted: number; } let lastQuery = 0; const wordCache: Record = {}; /** * Find and remove words that haven't been looked up for a day */ function flushCache(): void { const oldestWords: string[] = []; Object.keys(wordCache).forEach((word) => { const cached = wordCache[word]; let notOld = false; for (const person of cached.lastIndex) { if (person.lastTime > Date.now() - 24 * 60 * 60 * 1000) { notOld = true; break; } } if (!notOld) { oldestWords.push(word); } }); if (!oldestWords.length) { return; } oldestWords.forEach((item) => { delete wordCache[item]; }); logger.log( `[diction] Dictionary cleared of the previous words ${oldestWords.join( ', ' )}.` ); } @Configurable({ wordnik: '', cooldown: 5, limit: 5, }) class DictionPlugin extends Plugin { @EventListener('pluginUnload') public unloadEventHandler(plugin: string | Plugin): void { if (plugin === this.name || plugin === this) { this.emit('pluginUnloaded', this); } } @DependencyLoad('simplecommands') addCommands(cmd: any): void { const key = this.config.get('wordnik'); const rate = this.config.get('cooldown', 5); const limit = this.config.get('limit', 5); if (!key) { logger.warn('Wordnik define command is disabled due to no credentials.'); return; } cmd.registerCommand({ name: 'define', plugin: this.name, execute: async ( msg: IMessage, msr: MessageResolver, spec: any, prefix: string, ...simplified: any[] ): Promise => { const word = simplified.join(' '); const short = msg.source.supports( ProtocolFeatureFlag.SHORT_FORM_MESSAGING ); if (!word) { msg.resolve('Please specifiy a word or term!'); return true; } if (lastQuery > Date.now() - rate * 1000 && !wordCache[word]) { msg.resolve('You\'re doing that too fast!'); return true; } const skip = short ? 1 : 5; let chosenDefinitions: IDefinition[]; let definitionCount = 0; let definitionIndex = 0; // Check cached definitions const userTarget = msg.fullSenderID as string; if (wordCache[word]) { const cached = wordCache[word]; const alreadyAsked = cached.lastIndex.find( (item) => item.name === userTarget ); definitionCount = cached.definitions.length; if (alreadyAsked) { let startIndex = alreadyAsked.index; if (alreadyAsked.lastTime < Date.now() - 5 * 60 * 1000) { startIndex = 0; } const nextIndex = startIndex + skip >= cached.definitions.length ? 0 : startIndex + skip; alreadyAsked.lastTime = Date.now(); alreadyAsked.index = nextIndex; definitionIndex = startIndex; chosenDefinitions = cached.definitions.slice( startIndex, startIndex + skip ); } else { chosenDefinitions = cached.definitions.slice(0, skip); cached.lastIndex.push({ name: userTarget, index: skip, lastTime: Date.now(), }); } } else { // Request definition const encodedWord = encodeURIComponent(word); lastQuery = Date.now(); let response; try { const s = `https://api.wordnik.com/v4/word.json/${encodedWord}/definitions?limit=${ short ? limit : 100 }&api_key=${key}`; response = await httpGET(s); response = JSON.parse(response) as IApiResponse[]; } catch (e) { msg.resolve('Server did not respond.'); return true; } if (!response?.length || response.every((item) => !item.word)) { msg.resolve('No definitions found.'); return true; } const cached: IWordCache = { lastIndex: [ { name: userTarget, index: skip, lastTime: Date.now(), }, ], definitions: response .filter((entry) => entry.text) .map((data) => ({ partOfSpeech: data.partOfSpeech, text: data.text.replace(/(<([^>]+)>)/gi, ''), })), inserted: Date.now(), }; wordCache[word] = cached; chosenDefinitions = cached.definitions.slice(0, skip); definitionCount = cached.definitions.length; logger.log(`[diction] Dictionary cached the word "${word}"`); } if (short) { const { partOfSpeech, text } = chosenDefinitions[0]; msg.resolve( `(${definitionIndex + 1}/${definitionCount}) ${word}${ partOfSpeech ? ` - ${partOfSpeech}` : '' } - ${text}` ); } else { const generatedMessages: string[] = [word]; let lastPartOfSpeech = ''; for (const definition of chosenDefinitions) { if ( !lastPartOfSpeech || lastPartOfSpeech !== definition.partOfSpeech ) { lastPartOfSpeech = definition.partOfSpeech; generatedMessages.push( msg.source.format.format( 'bold', definition.partOfSpeech || 'other' ) ); } generatedMessages.push(` ${definition.text}`); } if (definitionIndex + skip < definitionCount) { generatedMessages.push( `(${definitionCount - definitionIndex - skip} more...)` ); } msg.resolve(generatedMessages.join('\n')); } return true; }, match: /define (\w*)/, aliases: ['df', 'word'], description: 'Find definitions for words. Call again to advance to next definition', usage: '', }); } @DependencyLoad('cron') public cronLoaded(cron: any): void { cron.registerTimer(this, '0 0 * * *', () => flushCache()); } } module.exports = DictionPlugin;