Use newer pridepack, and use Discord.js enums for setting up client.master
@@ -0,0 +1,15 @@ | |||||
DISCORD_BOT_TOKEN= | |||||
DISCORD_BOT_INTENTS=Guilds,GuildMessages,DirectMessages | |||||
DISCORD_BOT_PARTIALS=Channel,Reaction | |||||
GELBOORU_API_KEY= | |||||
GELBOORU_API_USER_ID= | |||||
GELBOORU_API_URL= | |||||
SAUCENAO_API_KEY= | |||||
SAUCENAO_API_URL= |
@@ -0,0 +1,9 @@ | |||||
{ | |||||
"root": true, | |||||
"extends": [ | |||||
"lxsmnsyc/typescript" | |||||
], | |||||
"parserOptions": { | |||||
"project": "./tsconfig.eslint.json" | |||||
} | |||||
} |
@@ -0,0 +1,111 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
lerna-debug.log* | |||||
# Diagnostic reports (https://nodejs.org/api/report.html) | |||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
# Runtime data | |||||
pids | |||||
*.pid | |||||
*.seed | |||||
*.pid.lock | |||||
# Directory for instrumented libs generated by jscoverage/JSCover | |||||
lib-cov | |||||
# Coverage directory used by tools like istanbul | |||||
coverage | |||||
*.lcov | |||||
# nyc test coverage | |||||
.nyc_output | |||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
.grunt | |||||
# Bower dependency directory (https://bower.io/) | |||||
bower_components | |||||
# node-waf configuration | |||||
.lock-wscript | |||||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
build/Release | |||||
# Dependency directories | |||||
node_modules/ | |||||
jspm_packages/ | |||||
# TypeScript v1 declaration files | |||||
typings/ | |||||
# TypeScript cache | |||||
*.tsbuildinfo | |||||
# Optional npm cache directory | |||||
.npm | |||||
# Optional eslint cache | |||||
.eslintcache | |||||
# Microbundle cache | |||||
.rpt2_cache/ | |||||
.rts2_cache_cjs/ | |||||
.rts2_cache_es/ | |||||
.rts2_cache_umd/ | |||||
# Optional REPL history | |||||
.node_repl_history | |||||
# Output of 'npm pack' | |||||
*.tgz | |||||
# Yarn Integrity file | |||||
.yarn-integrity | |||||
# dotenv environment variables file | |||||
.env | |||||
.env.production | |||||
.env.development | |||||
# parcel-bundler cache (https://parceljs.org/) | |||||
.cache | |||||
# Next.js build output | |||||
.next | |||||
# Nuxt.js build / generate output | |||||
.nuxt | |||||
dist | |||||
# Gatsby files | |||||
.cache/ | |||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js | |||||
# https://nextjs.org/blog/next-9-1#public-directory-support | |||||
# public | |||||
# vuepress build output | |||||
.vuepress/dist | |||||
# Serverless directories | |||||
.serverless/ | |||||
# FuseBox cache | |||||
.fusebox/ | |||||
# DynamoDB Local files | |||||
.dynamodb/ | |||||
# TernJS port file | |||||
.tern-port | |||||
.npmrc | |||||
.data | |||||
.image-replies.json | |||||
.text-replies.json | |||||
.idea/ |
@@ -0,0 +1,4 @@ | |||||
**CuuBot** | |||||
Upload anime images in the channel! Available commands are `post` and `help` (WIP). | |||||
@@ -0,0 +1,53 @@ | |||||
{ | |||||
"name": "cuubot", | |||||
"version": "0.0.0", | |||||
"files": [ | |||||
"dist", | |||||
"src" | |||||
], | |||||
"engines": { | |||||
"node": ">=12" | |||||
}, | |||||
"keywords": [ | |||||
"pridepack" | |||||
], | |||||
"devDependencies": { | |||||
"@types/node": "^18.14.1", | |||||
"eslint": "^8.35.0", | |||||
"eslint-config-lxsmnsyc": "^0.5.0", | |||||
"pridepack": "2.4.4", | |||||
"tslib": "^2.5.0", | |||||
"typescript": "^4.9.5", | |||||
"vitest": "^0.28.1" | |||||
}, | |||||
"dependencies": { | |||||
"discord.js": "^14.7.1", | |||||
"dotenv": "^16.0.3", | |||||
"fetch-ponyfill": "^7.1.0" | |||||
}, | |||||
"scripts": { | |||||
"prepublishOnly": "pridepack clean && pridepack build", | |||||
"build": "pridepack build", | |||||
"type-check": "pridepack check", | |||||
"lint": "pridepack lint", | |||||
"clean": "pridepack clean", | |||||
"watch": "pridepack watch", | |||||
"start": "pridepack start", | |||||
"dev": "pridepack dev", | |||||
"test": "vitest" | |||||
}, | |||||
"private": true, | |||||
"description": "CuuBot", | |||||
"repository": { | |||||
"url": "https://code.modal.sh/TheoryOfNekomata/cuubot", | |||||
"type": "git" | |||||
}, | |||||
"homepage": "https://code.modal.sh/TheoryOfNekomata/cuubot", | |||||
"bugs": { | |||||
"url": "https://code.modal.sh/TheoryOfNekomata/cuubot/issues" | |||||
}, | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"publishConfig": { | |||||
"access": "restricted" | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2018" | |||||
} |
@@ -0,0 +1,10 @@ | |||||
import { Client } from 'discord.js'; | |||||
import * as config from './config'; | |||||
const CLIENT = new Client({ | |||||
intents: config.discord.botIntents, | |||||
partials: config.discord.botPartials, | |||||
}); | |||||
export default CLIENT; |
@@ -0,0 +1,11 @@ | |||||
import { readFile } from 'fs/promises'; | |||||
import { Message } from 'discord.js'; | |||||
export default async (message: Message): Promise<void> => { | |||||
await message.reply('Sent you a DM <:chinesedoge:649753972395081788>'); | |||||
const readmeContentBuffer = await readFile('README.md'); | |||||
const readmeContentString = readmeContentBuffer.toString('utf-8'); | |||||
await message.author.send({ | |||||
content: readmeContentString, | |||||
}); | |||||
}; |
@@ -0,0 +1,55 @@ | |||||
/* eslint-disable import/prefer-default-export */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-call */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | |||||
import { Message } from 'discord.js'; | |||||
import * as database from '../utils/database'; | |||||
import * as anime from '../functions/anime'; | |||||
import * as sources from '../utils/sources'; | |||||
export default async (message: Message, ...args: string[]): Promise<void> => { | |||||
if (args.length <= 0) { | |||||
const data = await database.load(message.author.id); | |||||
if (data.length > 0) { | |||||
await message.channel.send({ | |||||
embeds: data.map((d) => ({ | |||||
title: 'CuuBot', | |||||
image: { | |||||
url: d.url, | |||||
}, | |||||
})), | |||||
}); | |||||
await database.remove(message.author.id); | |||||
} | |||||
return; | |||||
} | |||||
if (args.join(' ').toLowerCase() === 'cuu') { | |||||
const data = await sources.fetchBooru('thighhighs'); | |||||
const NOT_FOUND_MESSAGES = [ | |||||
'No images found.', | |||||
]; | |||||
if (data.post.length <= 0) { | |||||
await message.channel.send( | |||||
NOT_FOUND_MESSAGES[Math.floor(Math.random() * NOT_FOUND_MESSAGES.length)] | |||||
); | |||||
return; | |||||
} | |||||
await message.channel.send({ | |||||
embeds: [ | |||||
{ | |||||
title: 'CuuBot', | |||||
image: { | |||||
url: data.post[0].file_url as string, | |||||
}, | |||||
}, | |||||
], | |||||
}); | |||||
return; | |||||
} | |||||
const [subject] = args; | |||||
await anime.replyRandomImage(message, subject); | |||||
}; |
@@ -0,0 +1,40 @@ | |||||
import { | |||||
BitFieldResolvable, | |||||
GatewayIntentsString, | |||||
IntentsBitField, | |||||
Partials, | |||||
} from 'discord.js'; | |||||
// eslint-disable-next-line @typescript-eslint/no-namespace | |||||
export namespace discord { | |||||
export const botIntents = ( | |||||
(process.env.DISCORD_BOT_INTENTS as string) | |||||
.split(',') | |||||
.map((s) => ( | |||||
Object.entries(IntentsBitField.Flags).find(([key]) => key === s)?.[1] | |||||
)) | |||||
.filter((v) => typeof v === 'number') as unknown as BitFieldResolvable<GatewayIntentsString, number> | |||||
); | |||||
export const botPartials = ( | |||||
(process.env.DISCORD_BOT_PARTIALS as string) | |||||
.split(',') | |||||
.map((s) => ( | |||||
Object.entries(Partials) | |||||
.find(([key]) => key === s)?.[1] | |||||
)) | |||||
.filter((v) => typeof v === 'number') as unknown as Partials[] | |||||
); | |||||
} | |||||
// eslint-disable-next-line @typescript-eslint/no-namespace | |||||
export namespace gelbooru { | |||||
export const apiUrl = process.env.GELBOORU_API_URL as string; | |||||
export const apiKey = process.env.GELBOORU_API_KEY as string; | |||||
export const userId = process.env.GELBOORU_API_USER_ID as string; | |||||
} | |||||
// eslint-disable-next-line @typescript-eslint/no-namespace | |||||
export namespace saucenao { | |||||
export const apiUrl = process.env.SAUCENAO_API_URL as string; | |||||
export const apiKey = process.env.SAUCENAO_API_KEY as string; | |||||
} |
@@ -0,0 +1,92 @@ | |||||
/* eslint-disable import/prefer-default-export */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-call */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | |||||
import { readFile } from 'fs/promises'; | |||||
import { GuildEmoji, Message } from 'discord.js'; | |||||
import * as sources from '../utils/sources'; | |||||
import * as database from '../utils/database'; | |||||
type ImageReply = { | |||||
tags: (string | string[])[], | |||||
emojiName?: string[], | |||||
content?: string, | |||||
} | |||||
export const listenForImagesAndReply = async (message: Message): Promise<void> => { | |||||
const promises = Array | |||||
.from(message.attachments.values()) | |||||
.filter((a) => a.url.endsWith('.jpg') || a.url.endsWith('.jpeg') || a.url.endsWith('.png')) | |||||
.map(async (a) => sources.reverseSearch(a.url)); | |||||
const reverseSearchData = await Promise.all(promises); | |||||
const imageInfoPromises = reverseSearchData.map(async (d) => { | |||||
const [booruResult] = d.results; | |||||
if (!booruResult) { | |||||
return null; | |||||
} | |||||
return sources.fetchImage(booruResult.data.gelbooru_id.toString()); | |||||
}); | |||||
const imageInfos = await Promise.all(imageInfoPromises); | |||||
const tags = imageInfos.filter((i) => Boolean(i)).map((info) => info.tags.split(' ')).flat(); | |||||
const repliesBuffer = await readFile('.image-replies.json'); | |||||
const repliesString = repliesBuffer.toString('utf-8'); | |||||
const replies = JSON.parse(repliesString) as ImageReply[]; | |||||
const theReply = replies.reduce<ImageReply | null>( | |||||
(chosenReply, reply) => { | |||||
if (chosenReply === null && reply.tags.some((t) => { | |||||
if (Array.isArray(t)) { | |||||
return t.every((tt) => tags.includes(tt)); | |||||
} | |||||
return tags.includes(t); | |||||
})) { | |||||
return reply; | |||||
} | |||||
return chosenReply; | |||||
}, | |||||
null, | |||||
); | |||||
console.log(`${message.id}: ${tags.join(' ')}`); | |||||
if (theReply) { | |||||
const { emojiName: emojiNames = [], content: replyContent = '' } = theReply; | |||||
console.log([...emojiNames.map((em) => `+:${em}:`), replyContent].join(' ')); | |||||
if (emojiNames.length > 0) { | |||||
const emojis = emojiNames | |||||
.map((emName) => ( | |||||
message.guild?.emojis.cache.find((em) => em.name === emName) | |||||
)) | |||||
.filter((em) => Boolean(em)) as GuildEmoji[]; | |||||
await Promise.all(emojis.map((em) => message.react(em))); | |||||
} | |||||
if (theReply.content) { | |||||
await message.reply(theReply.content); | |||||
} | |||||
} | |||||
}; | |||||
export const replyRandomImage = async (message: Message, topic: string): Promise<void> => { | |||||
const data = await sources.fetchBooru(topic); | |||||
await message.channel.send({ | |||||
embeds: [ | |||||
{ | |||||
title: 'CuuBot', | |||||
image: { | |||||
url: data.post[0].file_url as string, | |||||
}, | |||||
}, | |||||
], | |||||
}); | |||||
}; | |||||
export const queueImage = async (message: Message): Promise<void> => { | |||||
const promises = Array | |||||
.from(message.attachments.values()) | |||||
.map(async (attachment) => database.save(message.author.id, { url: attachment.url })); | |||||
await Promise.all(promises); | |||||
await message.react('✅'); | |||||
}; |
@@ -0,0 +1,46 @@ | |||||
/* eslint-disable import/prefer-default-export */ | |||||
import { GuildEmoji, Message } from 'discord.js'; | |||||
import { readFile } from 'fs/promises'; | |||||
type TextReply = { | |||||
keywords: (string | string[])[], | |||||
emojiName?: string[], | |||||
content?: string, | |||||
} | |||||
export const listenForKeywordsAndReply = async (message: Message): Promise<void> => { | |||||
const repliesBuffer = await readFile('.text-replies.json'); | |||||
const repliesString = repliesBuffer.toString('utf-8'); | |||||
const replies = JSON.parse(repliesString) as TextReply[]; | |||||
const theReply = replies.reduce<TextReply | null>( | |||||
(chosenReply, reply) => { | |||||
if (chosenReply === null && reply.keywords.some((t) => { | |||||
if (Array.isArray(t)) { | |||||
return t.every((tt) => message.content === tt); | |||||
} | |||||
return message.content === t; | |||||
})) { | |||||
return reply; | |||||
} | |||||
return chosenReply; | |||||
}, | |||||
null, | |||||
); | |||||
if (theReply) { | |||||
const emojiNames = theReply.emojiName; | |||||
if (emojiNames) { | |||||
const emojis = emojiNames | |||||
.map((emName) => ( | |||||
message.guild?.emojis.cache.find((em) => em.name === emName) | |||||
)) | |||||
.filter((em) => Boolean(em)) as GuildEmoji[]; | |||||
await Promise.all(emojis.map((em) => message.react(em))); | |||||
} | |||||
if (theReply.content) { | |||||
await message.channel.send(theReply.content); | |||||
} | |||||
} | |||||
}; |
@@ -0,0 +1,2 @@ | |||||
import './handlers/ready'; | |||||
import './handlers/messageCreate'; |
@@ -0,0 +1,53 @@ | |||||
/* eslint-disable import/prefer-default-export */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-call */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | |||||
import { ChannelType } from 'discord.js'; | |||||
import CLIENT from '../client'; | |||||
import * as anime from '../functions/anime'; | |||||
import * as meme from '../functions/meme'; | |||||
import post from '../commands/post'; | |||||
import help from '../commands/help'; | |||||
CLIENT.on('messageCreate', async (message) => { | |||||
if (!CLIENT.user) { | |||||
return; | |||||
} | |||||
if (message.author.id === CLIENT.user.id) { | |||||
// Do not listen to own messages. | |||||
return; | |||||
} | |||||
const content = message.content?.replaceAll(/\s\s+/g, ' ').toLowerCase() ?? ''; | |||||
if (message.channel.type === ChannelType.DM) { | |||||
const [command] = content.split(' '); | |||||
if (command === 'save') { | |||||
await anime.queueImage(message); | |||||
} | |||||
return; | |||||
} | |||||
if (content.startsWith(`<@!${CLIENT.user.id}>`) || content.startsWith(`<@${CLIENT.user.id}>`)) { | |||||
const [, command, ...args] = content.split(' '); | |||||
if (command === 'post') { | |||||
await post(message, ...args); | |||||
return; | |||||
} | |||||
if (command === 'help') { | |||||
await help(message); | |||||
return; | |||||
} | |||||
return; | |||||
} | |||||
await meme.listenForKeywordsAndReply(message); | |||||
await anime.listenForImagesAndReply(message); | |||||
}); |
@@ -0,0 +1,5 @@ | |||||
import CLIENT from '../client'; | |||||
CLIENT.on('ready', () => { | |||||
console.log('client ready'); | |||||
}); |
@@ -0,0 +1,18 @@ | |||||
import CLIENT from './client'; | |||||
import './handlers'; | |||||
if (!process.env.DISCORD_BOT_TOKEN) { | |||||
console.error('No bot token specified.'); | |||||
process.exit(1); | |||||
} | |||||
CLIENT.login(process.env.DISCORD_BOT_TOKEN) | |||||
.then(() => { | |||||
if (CLIENT.user) { | |||||
console.log(`Bot client logged in as ${CLIENT.user.tag}`); | |||||
} | |||||
}) | |||||
.catch((err: Error) => { | |||||
console.error(err); | |||||
process.exit(1); | |||||
}); |
@@ -0,0 +1,98 @@ | |||||
import { | |||||
createWriteStream, | |||||
createReadStream, | |||||
promises, | |||||
ReadStream, | |||||
} from 'fs'; | |||||
import readline from 'readline'; | |||||
export type Item = { | |||||
url: string, | |||||
reactions?: string[], | |||||
} | |||||
export const save = async (userId: string, item: Item): Promise<void> => { | |||||
try { | |||||
await promises.stat('.data'); | |||||
} catch { | |||||
await promises.mkdir('.data'); | |||||
} | |||||
return new Promise((resolve, reject) => { | |||||
const ws = createWriteStream(`.data/${userId}.txt`, { flags: 'a' }); | |||||
ws.on('error', reject); | |||||
ws.on('finish', resolve); | |||||
ws.end(`${[item.url, ...(item.reactions || [])].join(' ')}\n`); | |||||
}); | |||||
}; | |||||
export const load = async (userId: string, count = 1): Promise<Item[]> => { | |||||
try { | |||||
await promises.stat('.data'); | |||||
} catch { | |||||
await promises.mkdir('.data'); | |||||
} | |||||
return new Promise<Item[]>((resolve, reject) => { | |||||
let rs: ReadStream; | |||||
const items: Item[] = []; | |||||
try { | |||||
rs = createReadStream(`.data/${userId}.txt`, { flags: 'r' }); | |||||
rs.on('error', reject); | |||||
const rl = readline.createInterface(rs); | |||||
rl.on('line', (line) => { | |||||
if (items.length >= count) { | |||||
return; | |||||
} | |||||
const [url, ...reactions] = line.split(' '); | |||||
items.push({ | |||||
url, | |||||
reactions, | |||||
}); | |||||
}); | |||||
rl.on('close', () => { | |||||
resolve(items); | |||||
}); | |||||
} catch { | |||||
// noop | |||||
} | |||||
}); | |||||
}; | |||||
export const remove = async (userId: string, count = 1): Promise<void> => { | |||||
try { | |||||
await promises.stat('.data'); | |||||
} catch { | |||||
await promises.mkdir('.data'); | |||||
} | |||||
return new Promise((resolve, reject) => { | |||||
let rs: ReadStream; | |||||
let lines = 0; | |||||
try { | |||||
rs = createReadStream(`.data/${userId}.txt`, { flags: 'r' }); | |||||
rs.on('error', reject); | |||||
const rl = readline.createInterface(rs); | |||||
const ws = createWriteStream(`.data/${userId}.txt.tmp`, { flags: 'w' }); | |||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises | |||||
ws.on('close', async () => { | |||||
await promises.unlink(`.data/${userId}.txt`); | |||||
await promises.rename(`.data/${userId}.txt.tmp`, `.data/${userId}.txt`); | |||||
resolve(); | |||||
}); | |||||
rl.on('line', (line) => { | |||||
lines += 1; | |||||
if (lines <= count) { | |||||
return; | |||||
} | |||||
ws.write(`${line}\n`); | |||||
}); | |||||
rl.on('close', () => { | |||||
ws.close(); | |||||
}); | |||||
} catch (e) { | |||||
// noop | |||||
reject(e); | |||||
} | |||||
}); | |||||
}; |
@@ -0,0 +1,78 @@ | |||||
/* eslint-disable import/prefer-default-export */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-call */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | |||||
/* eslint-disable @typescript-eslint/no-unsafe-return */ | |||||
import fetchPonyfill from 'fetch-ponyfill'; | |||||
import * as config from '../config'; | |||||
export const fetchBooru = async (q: string, page = 0) => { | |||||
if (q.trim().length <= 0) { | |||||
throw new Error('Specify search terms'); | |||||
} | |||||
const url = new URL(config.gelbooru.apiUrl); | |||||
const search = new URLSearchParams({ | |||||
page: 'dapi', | |||||
s: 'post', | |||||
api_key: config.gelbooru.apiKey, | |||||
user_id: config.gelbooru.userId, | |||||
q: 'index', | |||||
json: '1', | |||||
tags: encodeURIComponent(q), | |||||
pid: page.toString(), | |||||
}); | |||||
url.search = search.toString(); | |||||
const { fetch } = fetchPonyfill(); | |||||
const response = await fetch(url.toString()); | |||||
if (response.ok) { | |||||
const data = await response.json(); | |||||
console.log('OK', data); | |||||
return data; | |||||
} | |||||
const data = await response.text(); | |||||
console.log('NOT OK', data); | |||||
throw new Error('Gelbooru API error'); | |||||
}; | |||||
export const fetchImage = async (id: string) => { | |||||
const url = new URL(config.gelbooru.apiUrl); | |||||
const search = new URLSearchParams({ | |||||
page: 'dapi', | |||||
s: 'post', | |||||
api_key: config.gelbooru.apiKey, | |||||
user_id: config.gelbooru.userId, | |||||
q: 'index', | |||||
json: '1', | |||||
id, | |||||
}); | |||||
url.search = search.toString(); | |||||
const { fetch } = fetchPonyfill(); | |||||
const response = await fetch(url.toString()); | |||||
if (response.ok) { | |||||
const data = await response.json(); | |||||
return data.post[0]; | |||||
} | |||||
throw new Error('Gelbooru API error'); | |||||
}; | |||||
export const reverseSearch = async (u: string) => { | |||||
const url = new URL(config.saucenao.apiUrl); | |||||
const search = new URLSearchParams({ | |||||
db: '25', | |||||
output_type: '2', | |||||
testmode: '1', | |||||
numres: '16', | |||||
api_key: config.saucenao.apiKey, | |||||
url: u, | |||||
}); | |||||
url.search = search.toString(); | |||||
const { fetch } = fetchPonyfill(); | |||||
const response = await fetch(url.toString()); | |||||
if (response.ok) { | |||||
return response.json(); | |||||
} | |||||
throw new Error('SauceNAO API error'); | |||||
}; |
@@ -0,0 +1,82 @@ | |||||
import { EventEmitter } from 'events'; | |||||
import CLIENT from '../src/client'; | |||||
import '../src/handlers'; | |||||
jest.mock('dotenv', () => ({ | |||||
config: () => { | |||||
process.env.DISCORD_BOT_TOKEN = 'token'; | |||||
process.env.DISCORD_BOT_INTENTS = 'GUILDS,GUILD_MESSAGES'; | |||||
}, | |||||
})); | |||||
jest.mock('discord.js', () => ({ | |||||
Client: class MockClient extends EventEmitter { | |||||
public user: any = {}; | |||||
public token = ''; | |||||
async login(token: string) { | |||||
this.token = token; | |||||
this.user = { | |||||
id: '0', | |||||
tag: 'User#0000', | |||||
}; | |||||
return Promise.resolve(token); | |||||
} | |||||
}, | |||||
})); | |||||
describe('Example', () => { | |||||
let defaultConsoleLog: typeof console.log; | |||||
beforeEach(() => { | |||||
defaultConsoleLog = console.log; | |||||
console.log = jest.fn(); | |||||
}); | |||||
afterEach(() => { | |||||
console.log = defaultConsoleLog; | |||||
}); | |||||
beforeEach(async () => { | |||||
await CLIENT.login('token'); | |||||
}); | |||||
it('should ensure logged in user exists', () => { | |||||
expect(CLIENT.user).toEqual({ | |||||
id: '0', | |||||
tag: 'User#0000', | |||||
}); | |||||
}); | |||||
it('should handle ready event', () => { | |||||
CLIENT.emit('ready', CLIENT); | |||||
expect(console.log).toBeCalledWith('client ready'); | |||||
}); | |||||
it('should handle messageCreate event by echoing user message', () => { | |||||
const payload: Record<string, any> = { | |||||
author: { | |||||
id: '1', | |||||
tag: 'Anon#1337', | |||||
}, | |||||
embeds: [], | |||||
content: 'Test content.', | |||||
mentions: { | |||||
users: new Map([ | |||||
['0', { | |||||
id: '0', | |||||
tag: 'User#0000', | |||||
}], | |||||
]), | |||||
}, | |||||
reply: jest.fn(), | |||||
}; | |||||
CLIENT.emit('messageCreate', payload as any); | |||||
expect(payload.reply).toBeCalledWith({ | |||||
embeds: [], | |||||
content: 'Test content.', | |||||
}); | |||||
}); | |||||
}); |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types", "test"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["DOM", "ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "es2018" | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"include": ["src", "types"], | |||||
"compilerOptions": { | |||||
"module": "ESNext", | |||||
"lib": ["ESNext"], | |||||
"importHelpers": true, | |||||
"declaration": true, | |||||
"sourceMap": true, | |||||
"rootDir": "./src", | |||||
"strict": true, | |||||
"noUnusedLocals": true, | |||||
"noUnusedParameters": true, | |||||
"noImplicitReturns": true, | |||||
"noFallthroughCasesInSwitch": true, | |||||
"moduleResolution": "node", | |||||
"jsx": "react", | |||||
"esModuleInterop": true, | |||||
"target": "es2018" | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
import { Client } from 'discord.js'; | |||||
declare const CLIENT: Client<boolean>; | |||||
export default CLIENT; |
@@ -0,0 +1,3 @@ | |||||
import { Message } from 'discord.js'; | |||||
declare const _default: (message: Message) => Promise<void>; | |||||
export default _default; |
@@ -0,0 +1,3 @@ | |||||
import { Message } from 'discord.js'; | |||||
declare const _default: (message: Message, ...args: string[]) => Promise<void>; | |||||
export default _default; |
@@ -0,0 +1,14 @@ | |||||
import { BitFieldResolvable, Partials } from 'discord.js'; | |||||
export declare namespace discord { | |||||
const botIntents: BitFieldResolvable<"Guilds" | "GuildMembers" | "GuildModeration" | "GuildBans" | "GuildEmojisAndStickers" | "GuildIntegrations" | "GuildWebhooks" | "GuildInvites" | "GuildVoiceStates" | "GuildPresences" | "GuildMessages" | "GuildMessageReactions" | "GuildMessageTyping" | "DirectMessages" | "DirectMessageReactions" | "DirectMessageTyping" | "MessageContent" | "GuildScheduledEvents" | "AutoModerationConfiguration" | "AutoModerationExecution", number>; | |||||
const botPartials: Partials[]; | |||||
} | |||||
export declare namespace gelbooru { | |||||
const apiUrl: string; | |||||
const apiKey: string; | |||||
const userId: string; | |||||
} | |||||
export declare namespace saucenao { | |||||
const apiUrl: string; | |||||
const apiKey: string; | |||||
} |
@@ -0,0 +1,4 @@ | |||||
import { Message } from 'discord.js'; | |||||
export declare const listenForImagesAndReply: (message: Message) => Promise<void>; | |||||
export declare const replyRandomImage: (message: Message, topic: string) => Promise<void>; | |||||
export declare const queueImage: (message: Message) => Promise<void>; |
@@ -0,0 +1,2 @@ | |||||
import { Message } from 'discord.js'; | |||||
export declare const listenForKeywordsAndReply: (message: Message) => Promise<void>; |
@@ -0,0 +1,2 @@ | |||||
import './handlers/ready'; | |||||
import './handlers/messageCreate'; |
@@ -0,0 +1 @@ | |||||
export {}; |
@@ -0,0 +1 @@ | |||||
export {}; |
@@ -0,0 +1 @@ | |||||
import './handlers'; |
@@ -0,0 +1,7 @@ | |||||
export type Item = { | |||||
url: string; | |||||
reactions?: string[]; | |||||
}; | |||||
export declare const save: (userId: string, item: Item) => Promise<void>; | |||||
export declare const load: (userId: string, count?: number) => Promise<Item[]>; | |||||
export declare const remove: (userId: string, count?: number) => Promise<void>; |
@@ -0,0 +1,3 @@ | |||||
export declare const fetchBooru: (q: string, page?: number) => Promise<any>; | |||||
export declare const fetchImage: (id: string) => Promise<any>; | |||||
export declare const reverseSearch: (u: string) => Promise<any>; |