Skip to content

Commit e250206

Browse files
authored
Merge pull request #220 from Gerrit0/discord-14
Discord v14, farewell Cookiecord
2 parents 7d64792 + 338777c commit e250206

28 files changed

+2281
-2774
lines changed

.husky/pre-commit

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env sh
2+
. "$(dirname -- "$0")/_/husky.sh"
3+
4+
npx pretty-quick --staged

.vscode/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
{
2-
"typescript.tsdk": "node_modules/typescript/lib"
2+
"typescript.tsdk": "node_modules/typescript/lib",
3+
"cSpell.words": [
4+
"algoliasearch",
5+
"autorole",
6+
"Cooldown",
7+
"leaderboard",
8+
"twoslash",
9+
"twoslasher"
10+
]
311
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# 2022-11-19
2+
3+
- Updated to Discord.js 14, removed Cookiecord to prevent future delays in updating versions.
4+
- The bot will now react on the configured autorole messages to indicate available roles.
5+
- Unhandled rejections will now only be ignored if `NODE_ENV` is set to `production`.
6+
- Removed admin `checkThreads` command as using it would result in the bot checking for closed threads twice as often until restarted.

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:16.14.0-alpine
1+
FROM node:16.18.1-alpine
22
WORKDIR /usr/src/app
33

44
COPY yarn.lock ./

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ See the [documentation](https://cookiecord.js.org/) for the framework we use.
1212

1313
We also have a docker-compose.yml for development, along with a .env.example.
1414

15-
**A quick note about the help channel system:** Please don't use it if you don't have a large server (10k+ members) as it will likely inconvenience your members rather than benefit them. We used a static channel system (#help-1 and #help-2) up until around 9,000 members, when we started to see issues arising (many questions being asked on top of eachother without answers).
15+
**A quick note about the help channel system:** Please only use it if you have a large server (10k+ members) as it will likely inconvenience your members rather than benefit them. We used a static channel system (#help-1 and #help-2) up until around 9,000 members, when we started to see issues arising (many questions being asked on top of each other without answers).
1616

1717
## Thanks!
1818

19-
- [ckie](https://github.com/ckiee) for writing the base for the bot and the amazing [framework](https://github.com/cookiecord/cookiecord) used!
19+
- [ckie](https://github.com/ckiee) for writing the base for the bot!
2020
- [Python Discord](https://github.com/python-discord) for heavily influencing our help channel system.

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ services:
2727
volumes:
2828
- 'postgres_data:/postgres/data'
2929
ports:
30-
- 5432
30+
- 5432:5432
3131

3232
volumes:
3333
postgres_data:

package.json

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,39 @@
44
"description": "Typescript Community Bot",
55
"main": "dist/index.js",
66
"dependencies": {
7-
"@typescript/twoslash": "^2.1.0",
8-
"algoliasearch": "^4.8.6",
9-
"cookiecord": "^0.8.18",
7+
"@typescript/twoslash": "^3.2.1",
8+
"algoliasearch": "^4.14.2",
9+
"discord.js": "^14.6.0",
1010
"dotenv-safe": "^8.2.0",
11-
"html-entities": "^2.3.2",
11+
"html-entities": "^2.3.3",
1212
"lz-string": "^1.4.4",
13-
"node-fetch": "^2.6.7",
14-
"npm-registry-fetch": "^9.0.0",
15-
"parse-duration": "^0.4.4",
16-
"pg": "^8.3.0",
17-
"prettier": "^2.2.1",
18-
"pretty-ms": "^7.0.0",
19-
"tar": "^6.1.0",
20-
"typeorm": "^0.2.25"
13+
"npm-registry-fetch": "^14.0.2",
14+
"parse-duration": "^1.0.2",
15+
"pg": "^8.8.0",
16+
"prettier": "^2.7.1",
17+
"pretty-ms": "^8.0.0",
18+
"tar": "^6.1.12",
19+
"typeorm": "^0.3.10",
20+
"undici": "^5.12.0"
2121
},
2222
"devDependencies": {
23-
"@types/dotenv-safe": "^8.1.0",
24-
"@types/lz-string": "^1.3.34",
25-
"@types/node": "^13.7.0",
26-
"@types/node-fetch": "^2.5.8",
27-
"@types/npm-registry-fetch": "^8.0.0",
28-
"@types/prettier": "^2.2.3",
29-
"@types/tar": "^4.0.4",
30-
"@types/ws": "^7.2.1",
31-
"husky": "^4.2.5",
32-
"pretty-quick": "^2.0.1",
33-
"ts-node-dev": "^1.0.0-pre.60",
34-
"typescript": "^4.1.3"
35-
},
36-
"husky": {
37-
"hooks": {
38-
"pre-commit": "pretty-quick --staged"
39-
}
23+
"@types/dotenv-safe": "8.1.2",
24+
"@types/lz-string": "1.3.34",
25+
"@types/node": "16.18.3",
26+
"@types/npm-registry-fetch": "8.0.4",
27+
"@types/prettier": "2.7.1",
28+
"@types/tar": "6.1.3",
29+
"@types/ws": "8.5.3",
30+
"husky": "^8.0.2",
31+
"pretty-quick": "^3.1.3",
32+
"ts-node-dev": "^2.0.0",
33+
"typescript": "^4.9.3"
4034
},
4135
"scripts": {
4236
"start": "ts-node-dev --respawn src",
4337
"build": "tsc",
4438
"lint": "prettier --check \"src/**/*.ts\"",
45-
"lint:fix": "prettier \"src/**/*.ts\" --write "
39+
"lint:fix": "prettier \"src/**/*.ts\" --write ",
40+
"prepare": "husky install"
4641
}
4742
}

src/bot.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Message, Client, User, GuildMember } from 'discord.js';
2+
import { botAdmins, prefixes, trustedRoleId } from './env';
3+
4+
export interface CommandRegistration {
5+
aliases: string[];
6+
description?: string;
7+
listener: (msg: Message, content: string) => Promise<void>;
8+
}
9+
10+
interface Command {
11+
admin: boolean;
12+
aliases: string[];
13+
description?: string;
14+
listener: (msg: Message, content: string) => Promise<void>;
15+
}
16+
17+
export class Bot {
18+
commands = new Map<string, Command>();
19+
20+
constructor(public client: Client<true>) {
21+
client.on('messageCreate', msg => {
22+
const triggerWithPrefix = msg.content.split(/\s/)[0];
23+
const matchingPrefix = prefixes.find(p =>
24+
triggerWithPrefix.startsWith(p),
25+
);
26+
if (matchingPrefix) {
27+
const content = msg.content
28+
.substring(triggerWithPrefix.length + 1)
29+
.trim();
30+
31+
const command = this.getByTrigger(
32+
triggerWithPrefix.substring(matchingPrefix.length),
33+
);
34+
35+
if (!command || (command.admin && !this.isAdmin(msg.author))) {
36+
return;
37+
}
38+
command.listener(msg, content).catch(err => {
39+
this.client.emit('error', err);
40+
});
41+
}
42+
});
43+
}
44+
45+
registerCommand(registration: CommandRegistration) {
46+
const command: Command = {
47+
...registration,
48+
admin: false,
49+
};
50+
for (const a of command.aliases) {
51+
this.commands.set(a, command);
52+
}
53+
}
54+
55+
registerAdminCommand(registration: CommandRegistration) {
56+
const command: Command = {
57+
...registration,
58+
admin: true,
59+
};
60+
for (const a of command.aliases) {
61+
this.commands.set(a, command);
62+
}
63+
}
64+
65+
getByTrigger(trigger: string): Command | undefined {
66+
return this.commands.get(trigger);
67+
}
68+
69+
isMod(member: GuildMember | null) {
70+
return member?.permissions.has('ManageMessages') ?? false;
71+
}
72+
73+
isAdmin(user: User) {
74+
return botAdmins.includes(user.id);
75+
}
76+
77+
getTrustedMemberError(msg: Message) {
78+
if (!msg.guild || !msg.member || !msg.channel.isTextBased()) {
79+
return ":warning: you can't use that command here.";
80+
}
81+
82+
if (
83+
!msg.member.roles.cache.has(trustedRoleId) &&
84+
!msg.member.permissions.has('ManageMessages')
85+
) {
86+
return ":warning: you don't have permission to use that command.";
87+
}
88+
}
89+
90+
async getTargetUser(msg: Message): Promise<User | undefined> {
91+
const query = msg.content.split(/\s/)[1];
92+
93+
const mentioned = msg.mentions.members?.first()?.user;
94+
if (mentioned) return mentioned;
95+
96+
if (!query) return;
97+
98+
// Search by ID
99+
const queriedUser = await this.client.users
100+
.fetch(query)
101+
.catch(() => undefined);
102+
if (queriedUser) return queriedUser;
103+
104+
// Search by name, likely a better way to do this...
105+
for (const user of this.client.users.cache.values()) {
106+
if (user.tag === query || user.username === query) {
107+
return user;
108+
}
109+
}
110+
}
111+
}

src/db.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Connection, createConnection } from 'typeorm';
1+
import { DataSource } from 'typeorm';
22
import { dbUrl } from './env';
33
import { Rep } from './entities/Rep';
44
import { HelpThread } from './entities/HelpThread';
55
import { Snippet } from './entities/Snippet';
66

7-
let db: Connection | undefined;
7+
let db: DataSource | undefined;
88
export async function getDB() {
99
if (db) return db;
1010

@@ -21,14 +21,15 @@ export async function getDB() {
2121
}
2222
: {};
2323

24-
db = await createConnection({
24+
db = new DataSource({
2525
type: 'postgres',
2626
url: dbUrl,
2727
synchronize: true,
2828
logging: false,
2929
entities: [Rep, HelpThread, Snippet],
3030
...extraOpts,
3131
});
32+
await db.initialize();
3233
console.log('Connected to DB');
3334
return db;
3435
}

src/index.ts

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,69 @@
1-
import { token, botAdmins, prefixes } from './env';
2-
import CookiecordClient from 'cookiecord';
3-
import { Intents } from 'discord.js';
1+
import { Client, GatewayIntentBits, Partials } from 'discord.js';
2+
import { Bot } from './bot';
43
import { getDB } from './db';
4+
import { token } from './env';
55
import { hookLog } from './log';
66

7-
import { AutoroleModule } from './modules/autorole';
8-
import { EtcModule } from './modules/etc';
9-
import { HelpThreadModule } from './modules/helpthread';
10-
import { PlaygroundModule } from './modules/playground';
11-
import { RepModule } from './modules/rep';
12-
import { TwoslashModule } from './modules/twoslash';
13-
import { HelpModule } from './modules/help';
14-
import { SnippetModule } from './modules/snippet';
15-
import { HandbookModule } from './modules/handbook';
16-
import { ModModule } from './modules/mod';
7+
import { autoroleModule } from './modules/autorole';
8+
import { etcModule } from './modules/etc';
9+
import { handbookModule } from './modules/handbook';
10+
import { helpModule } from './modules/help';
11+
import { modModule } from './modules/mod';
12+
import { playgroundModule } from './modules/playground';
13+
import { repModule } from './modules/rep';
14+
import { twoslashModule } from './modules/twoslash';
15+
import { snippetModule } from './modules/snippet';
16+
import { helpThreadModule } from './modules/helpthread';
1717

18-
const client = new CookiecordClient(
19-
{
20-
botAdmins,
21-
prefix: prefixes,
18+
const client = new Client({
19+
partials: [
20+
Partials.Reaction,
21+
Partials.Message,
22+
Partials.User,
23+
Partials.Channel,
24+
],
25+
allowedMentions: {
26+
parse: ['users', 'roles'],
2227
},
23-
{
24-
partials: ['REACTION', 'MESSAGE', 'USER', 'CHANNEL'],
25-
allowedMentions: {
26-
parse: ['users', 'roles'],
27-
},
28-
intents: new Intents([
29-
'GUILDS',
30-
'GUILD_MESSAGES',
31-
'GUILD_MEMBERS',
32-
'GUILD_MESSAGE_REACTIONS',
33-
'DIRECT_MESSAGES',
34-
]),
35-
},
36-
).setMaxListeners(Infinity);
37-
38-
for (const mod of [
39-
AutoroleModule,
40-
EtcModule,
41-
HelpThreadModule,
42-
PlaygroundModule,
43-
RepModule,
44-
TwoslashModule,
45-
HelpModule,
46-
SnippetModule,
47-
HandbookModule,
48-
ModModule,
49-
]) {
50-
client.registerModule(mod);
51-
}
28+
intents: [
29+
GatewayIntentBits.Guilds,
30+
GatewayIntentBits.GuildMessages,
31+
GatewayIntentBits.GuildMembers,
32+
GatewayIntentBits.GuildMessageReactions,
33+
GatewayIntentBits.DirectMessages,
34+
GatewayIntentBits.MessageContent,
35+
],
36+
}).setMaxListeners(Infinity);
5237

53-
getDB(); // prepare the db for later
38+
getDB().then(() => client.login(token));
5439

55-
client.login(token);
56-
client.on('ready', () => {
40+
client.on('ready', async () => {
41+
const bot = new Bot(client);
5742
console.log(`Logged in as ${client.user?.tag}`);
58-
hookLog(client);
43+
await hookLog(client);
44+
45+
for (const mod of [
46+
autoroleModule,
47+
etcModule,
48+
helpThreadModule,
49+
playgroundModule,
50+
repModule,
51+
twoslashModule,
52+
helpModule,
53+
snippetModule,
54+
handbookModule,
55+
modModule,
56+
]) {
57+
await mod(bot);
58+
}
5959
});
6060

61-
process.on('unhandledRejection', e => {
62-
console.error('Unhandled rejection', e);
61+
client.on('error', error => {
62+
console.error(error);
6363
});
64+
65+
if (process.env.NODE_ENV === 'production') {
66+
process.on('unhandledRejection', e => {
67+
console.error('Unhandled rejection', e);
68+
});
69+
}

0 commit comments

Comments
 (0)