plugins-evert/diction/plugin.ts
2024-06-07 23:06:50 +03:00

263 lines
6.9 KiB
TypeScript

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<string, IWordCache> = {};
/**
* 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<boolean> => {
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: '<word>',
});
}
@DependencyLoad('cron')
public cronLoaded(cron: any): void {
cron.registerTimer(this, '0 0 * * *', () => flushCache());
}
}
module.exports = DictionPlugin;