Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx pretty-quick --staged
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"cSpell.words": [
"algoliasearch",
"autorole",
"Cooldown",
"leaderboard",
"twoslash",
"twoslasher"
]
}
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 2022-11-19

- Updated to Discord.js 14, removed Cookiecord to prevent future delays in updating versions.
- The bot will now react on the configured autorole messages to indicate available roles.
- Unhandled rejections will now only be ignored if `NODE_ENV` is set to `production`.
- Removed admin `checkThreads` command as using it would result in the bot checking for closed threads twice as often until restarted.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:16.14.0-alpine
FROM node:16.18.1-alpine
WORKDIR /usr/src/app

COPY yarn.lock ./
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ See the [documentation](https://cookiecord.js.org/) for the framework we use.

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

**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).
**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).

## Thanks!

- [ckie](https://github.com/ckiee) for writing the base for the bot and the amazing [framework](https://github.com/cookiecord/cookiecord) used!
- [ckie](https://github.com/ckiee) for writing the base for the bot!
- [Python Discord](https://github.com/python-discord) for heavily influencing our help channel system.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ services:
volumes:
- 'postgres_data:/postgres/data'
ports:
- 5432
- 5432:5432
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly necessary, but useful if you want to run the bot locally

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this because it was giving me issues running this locally - seems like maybe we have mutually incompatible setups of some sort?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... according to https://docs.docker.com/compose/compose-file/compose-file-v3/#ports, without :5432 Docker picks a random port, which would work around your port in use issue. I suspect the port was actually in use.


volumes:
postgres_data:
55 changes: 25 additions & 30 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,39 @@
"description": "Typescript Community Bot",
"main": "dist/index.js",
"dependencies": {
"@typescript/twoslash": "^2.1.0",
"algoliasearch": "^4.8.6",
"cookiecord": "^0.8.18",
"@typescript/twoslash": "^3.2.1",
"algoliasearch": "^4.14.2",
"discord.js": "^14.6.0",
"dotenv-safe": "^8.2.0",
"html-entities": "^2.3.2",
"html-entities": "^2.3.3",
"lz-string": "^1.4.4",
"node-fetch": "^2.6.7",
"npm-registry-fetch": "^9.0.0",
"parse-duration": "^0.4.4",
"pg": "^8.3.0",
"prettier": "^2.2.1",
"pretty-ms": "^7.0.0",
"tar": "^6.1.0",
"typeorm": "^0.2.25"
"npm-registry-fetch": "^14.0.2",
"parse-duration": "^1.0.2",
"pg": "^8.8.0",
"prettier": "^2.7.1",
"pretty-ms": "^8.0.0",
"tar": "^6.1.12",
"typeorm": "^0.3.10",
"undici": "^5.12.0"
},
"devDependencies": {
"@types/dotenv-safe": "^8.1.0",
"@types/lz-string": "^1.3.34",
"@types/node": "^13.7.0",
"@types/node-fetch": "^2.5.8",
"@types/npm-registry-fetch": "^8.0.0",
"@types/prettier": "^2.2.3",
"@types/tar": "^4.0.4",
"@types/ws": "^7.2.1",
"husky": "^4.2.5",
"pretty-quick": "^2.0.1",
"ts-node-dev": "^1.0.0-pre.60",
"typescript": "^4.1.3"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
"@types/dotenv-safe": "8.1.2",
"@types/lz-string": "1.3.34",
"@types/node": "16.18.3",
"@types/npm-registry-fetch": "8.0.4",
"@types/prettier": "2.7.1",
"@types/tar": "6.1.3",
"@types/ws": "8.5.3",
"husky": "^8.0.2",
"pretty-quick": "^3.1.3",
"ts-node-dev": "^2.0.0",
"typescript": "^4.9.3"
},
"scripts": {
"start": "ts-node-dev --respawn src",
"build": "tsc",
"lint": "prettier --check \"src/**/*.ts\"",
"lint:fix": "prettier \"src/**/*.ts\" --write "
"lint:fix": "prettier \"src/**/*.ts\" --write ",
"prepare": "husky install"
}
}
111 changes: 111 additions & 0 deletions src/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Message, Client, User, GuildMember } from 'discord.js';
import { botAdmins, prefixes, trustedRoleId } from './env';

export interface CommandRegistration {
aliases: string[];
description?: string;
listener: (msg: Message, content: string) => Promise<void>;
}

interface Command {
admin: boolean;
aliases: string[];
description?: string;
listener: (msg: Message, content: string) => Promise<void>;
}

export class Bot {
commands = new Map<string, Command>();

constructor(public client: Client<true>) {
client.on('messageCreate', msg => {
const triggerWithPrefix = msg.content.split(/\s/)[0];
const matchingPrefix = prefixes.find(p =>
triggerWithPrefix.startsWith(p),
);
if (matchingPrefix) {
const content = msg.content
.substring(triggerWithPrefix.length + 1)
.trim();

const command = this.getByTrigger(
triggerWithPrefix.substring(matchingPrefix.length),
);

if (!command || (command.admin && !this.isAdmin(msg.author))) {
return;
}
command.listener(msg, content).catch(err => {
this.client.emit('error', err);
});
}
});
}

registerCommand(registration: CommandRegistration) {
const command: Command = {
...registration,
admin: false,
};
for (const a of command.aliases) {
this.commands.set(a, command);
}
}

registerAdminCommand(registration: CommandRegistration) {
const command: Command = {
...registration,
admin: true,
};
for (const a of command.aliases) {
this.commands.set(a, command);
}
}

getByTrigger(trigger: string): Command | undefined {
return this.commands.get(trigger);
}

isMod(member: GuildMember | null) {
return member?.permissions.has('ManageMessages') ?? false;
}

isAdmin(user: User) {
return botAdmins.includes(user.id);
}

getTrustedMemberError(msg: Message) {
if (!msg.guild || !msg.member || !msg.channel.isTextBased()) {
return ":warning: you can't use that command here.";
}

if (
!msg.member.roles.cache.has(trustedRoleId) &&
!msg.member.permissions.has('ManageMessages')
) {
return ":warning: you don't have permission to use that command.";
}
}

async getTargetUser(msg: Message): Promise<User | undefined> {
const query = msg.content.split(/\s/)[1];

const mentioned = msg.mentions.members?.first()?.user;
if (mentioned) return mentioned;

if (!query) return;

// Search by ID
const queriedUser = await this.client.users
.fetch(query)
.catch(() => undefined);
if (queriedUser) return queriedUser;

// Search by name, likely a better way to do this...
for (const user of this.client.users.cache.values()) {
if (user.tag === query || user.username === query) {
return user;
}
}
}
}
7 changes: 4 additions & 3 deletions src/db.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Connection, createConnection } from 'typeorm';
import { DataSource } from 'typeorm';
import { dbUrl } from './env';
import { Rep } from './entities/Rep';
import { HelpThread } from './entities/HelpThread';
import { Snippet } from './entities/Snippet';

let db: Connection | undefined;
let db: DataSource | undefined;
export async function getDB() {
if (db) return db;

Expand All @@ -21,14 +21,15 @@ export async function getDB() {
}
: {};

db = await createConnection({
db = new DataSource({
type: 'postgres',
url: dbUrl,
synchronize: true,
logging: false,
entities: [Rep, HelpThread, Snippet],
...extraOpts,
});
await db.initialize();
console.log('Connected to DB');
return db;
}
110 changes: 58 additions & 52 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,69 @@
import { token, botAdmins, prefixes } from './env';
import CookiecordClient from 'cookiecord';
import { Intents } from 'discord.js';
import { Client, GatewayIntentBits, Partials } from 'discord.js';
import { Bot } from './bot';
import { getDB } from './db';
import { token } from './env';
import { hookLog } from './log';

import { AutoroleModule } from './modules/autorole';
import { EtcModule } from './modules/etc';
import { HelpThreadModule } from './modules/helpthread';
import { PlaygroundModule } from './modules/playground';
import { RepModule } from './modules/rep';
import { TwoslashModule } from './modules/twoslash';
import { HelpModule } from './modules/help';
import { SnippetModule } from './modules/snippet';
import { HandbookModule } from './modules/handbook';
import { ModModule } from './modules/mod';
import { autoroleModule } from './modules/autorole';
import { etcModule } from './modules/etc';
import { handbookModule } from './modules/handbook';
import { helpModule } from './modules/help';
import { modModule } from './modules/mod';
import { playgroundModule } from './modules/playground';
import { repModule } from './modules/rep';
import { twoslashModule } from './modules/twoslash';
import { snippetModule } from './modules/snippet';
import { helpThreadModule } from './modules/helpthread';

const client = new CookiecordClient(
{
botAdmins,
prefix: prefixes,
const client = new Client({
partials: [
Partials.Reaction,
Partials.Message,
Partials.User,
Partials.Channel,
],
allowedMentions: {
parse: ['users', 'roles'],
},
{
partials: ['REACTION', 'MESSAGE', 'USER', 'CHANNEL'],
allowedMentions: {
parse: ['users', 'roles'],
},
intents: new Intents([
'GUILDS',
'GUILD_MESSAGES',
'GUILD_MEMBERS',
'GUILD_MESSAGE_REACTIONS',
'DIRECT_MESSAGES',
]),
},
).setMaxListeners(Infinity);

for (const mod of [
AutoroleModule,
EtcModule,
HelpThreadModule,
PlaygroundModule,
RepModule,
TwoslashModule,
HelpModule,
SnippetModule,
HandbookModule,
ModModule,
]) {
client.registerModule(mod);
}
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessageReactions,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent,
],
}).setMaxListeners(Infinity);

getDB(); // prepare the db for later
getDB().then(() => client.login(token));

client.login(token);
client.on('ready', () => {
client.on('ready', async () => {
const bot = new Bot(client);
console.log(`Logged in as ${client.user?.tag}`);
hookLog(client);
await hookLog(client);

for (const mod of [
autoroleModule,
etcModule,
helpThreadModule,
playgroundModule,
repModule,
twoslashModule,
helpModule,
snippetModule,
handbookModule,
modModule,
]) {
await mod(bot);
}
});

process.on('unhandledRejection', e => {
console.error('Unhandled rejection', e);
client.on('error', error => {
console.error(error);
});

if (process.env.NODE_ENV === 'production') {
process.on('unhandledRejection', e => {
console.error('Unhandled rejection', e);
});
}
Loading