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>; |