Skip to content

Commit

Permalink
feat(*): Discord.js v13 update (#353)
Browse files Browse the repository at this point in the history
* feat(adminCommands): write interaction objects

* feat(botCommands): write interaction objects

* feat(mainCommands): write interaction objects

* feat(settingsCommands): write interaction objects

* feat(interactions): add index files

* fix(interactions): remove setSettingCommand

* chore(deps): update Discord.js and remove Commando

* feat(interactions): add main index file

* feat(EventHandlers): remove and update event handlers

* feat(Client): break Commando out of Client and SettingProvider

* feat(applicationConfig): remove redundant settings

* feat(Client): update types

* refactor(types): remove argument types

* fix(*): change xID to xId

* fix(*): overloads and timers

* fix(Managers): rename add with _add

* fix(Managers): update and improve resolve methods

* fix(Managers): remove iterable constructor argument

* feat(MessageCreateEventHandler): default to development env

* feat(Commands): bind commands and factory in container

* feat(Dispatcher): implement dispatcher

* feat(bin): make update commands script

* feat(*): use Client Ready generic argument where possible

* feat(verificationService): move methods to own service

* chore(deps): update Discord.js to v13.2.0

* feat(argumentTypes): readd RobloxUserArgumentType

* feat(AroraClient): add Ready generic type parameter

* feat(Channel

* feat(commands): port first command

And do some style fixes

* refactor(commands): remove old ban commands

* feat(Dispatcher): update parseArgs to use custom validate/parse

* feat(arguments): port util and create argument class

* feat(dispatcher): use default value when specified

* feat(InteractionCreateEventHandler): reply to interaction with error

* refactor: move guild extensions to own class

* feat: add default-emoji argument type

* feat: add date and time argument types

* fix: use argument name in result when specified

* refactor: improve whitespace checks

* feat: implement GuildContextManager

* refactor: remove Role and TextChannel extensions

* refactor: pass context to structures and managers

* refactor: remove unused managers

* chore: formatting changes and fixes

* feat(Commands): port training commands to new structure

* refactor(Commands): port promote and demote commands

* refactor(Commands): port exile commands

* refactor(Commands): reformat and improve ported commands

* refactor(Commands): port persistent role commands

* refactor(Commands): port shout command

* fix(Interactions): correctly filter choices

* fix(bin): require reflect-metadata

* refactor(Commands): port bot commands

* refactor(Commands): port first few main commands

* refactor(Commands): port last main commands

* fix(RobloxUserArgumentType): verification data fetching

* refactor(ArgumentTypes): make structure argument type

* refactor: port groups commands and support sub command groups

* refactor: fixes and make types stricter

* refactor(ArgumentTypes): readd json-object and message argument types

* refactor(BansCommand): use of new (min|max)_value constraints

* fix(Interactions): change defaultPermission to default_permission

* fix(ChannelLinksInteraction): make command option names lowercase

* refactor(ArgumentTypes): add PanelArgumentType

* feat: implement union argument types

* refactor: some improvements

* refactor(Commands): port panel commands

* refactor(Commands): port channel links commands

* refactor: move channel links and persistent roles methods to own service

* fix: run yarn install

* fix: lint errors after merge

* fix: BaseCommandInteraction import

* fix(Client): improve and fix types

* fix: update commands

* refactor: port settings commands and fix types

* refactor: last settings commands

* refactor: fix all lint and tsc errors

* chore: update discord.js and api types and fix some type imports

* fix: client passing into command constructor

* refactor: improve constructor factory

* refactor: use Inversify's interfaces.Newable instead of Constructor type

* refactor: Dispatcher and SettingProvider client handling and utilise
numeric separators

* revert: move packetHandlerFactory back to Client

* refactor: improve types and fix poll command

* refactor: improve Client constructor and fix promise lints

* refactor: remove redundant Promise.resolve

* fix: remove double unref

* refactor: reformat cron.schedule and setTimeout call

* fix: styling

* refactor: overwrite _add in BaseManager to reduce ts-expect-error
directives

* feat: add category-channel and text-channel argument types

* fix: category-channel class name

* refactor: rename GuildContextX with GuildX

* fix: disallow overwriting id column on repository.save

* fix(interactions): change id options to string type

* feat(MessageCreateEventHandler): send message on successful deploy

* fix(TagCommand): fix formatting

* fix(Argument): correctly resolve union type

* fix(BaseCommand): bind this argument on calling command function

* refactor: move GuildSetting to constants

* fix(argumentTypes): correctly escape digit symbol

* refactor: move dependency injections to client

To avoid circular dependencies returning undefined.
Should come up with a better fix in the future.

* refactor(managers): import GuildContext as type only where possible

* fix(setActivityCommand): correctly transform ActivityTypes to choices

* fix(GroupsCommand): use correct key name

* fix(TagCommand): fix formatting when who is null

* refactor(Client): improve failSilently method typings

* chore(setActivityCommand): improve styling

* fix(Ticket) lint error

* feat(interactions): specify export types

* fix(package): move discord-api-types to dependencies

* feat: deploy commands in publish image workflow

* fix(Client): use PartialTypes type instead of value

* fix(settingsInteraction): make value option optional

* fix(Argument): correctly check if valid before parsing

* fix(SettingsCommand): enum check

* refactor(*): fully utilize Inversify and remove
inversify-inject-decorators (#410)

* feat(argumentTypes): add boolean and integer type

* chore: styling fixes

* refactor(interactions): improve folder structure

* fix: deploy commands in deploy job

* refactor: improve imports

* feat(argumentTypes): add custom-emoji argument type

* feat(eventHandlers): log command runs

* refactor(argumentTypes): don't parse when invalid

* feat(GuildGroupManager): make factory types stricter

* fix(package): change engine version constraint

* refactor: move types to own util file

* refactor: use ManagerFactory type where possible

* refactor: use Inversify's factory types where possible

* fix(container): commandFactory typings

* chore(deps): update discord-api-types to v0.33.0

* feat: use discord api v10

* chore(deps): update discord.js to v13.7.0

* fix: cachetype errors

* feat(Dispatcher): support attachment option type

* feat(commands): disable all commands by default

* feat: add database migration

* fix: revert change in workflow

* fix: actually use API v10 in updateCommands

* feat: don't disable status command by default
  • Loading branch information
guidojw authored May 16, 2022
1 parent 5242ef4 commit 42f4bc7
Show file tree
Hide file tree
Showing 285 changed files with 7,614 additions and 6,700 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
'sort-imports': 'error',
'unicorn/prefer-node-protocol': 'error',
// '@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/explicit-member-accessibility': 'error'
'@typescript-eslint/explicit-member-accessibility': 'error',
'@typescript-eslint/method-signature-style': 'off'
}
}
1 change: 1 addition & 0 deletions .github/workflows/continuous-delivery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ jobs:
cd /opt/docker/$PROJECT_NAME/$STAGE
docker-compose pull
docker-compose run --rm app yarn run typeorm migration:run
docker-compose run --rm app node bin/update-commands.js
docker-compose up -d
- name: Finalize Sentry release
Expand Down
16 changes: 16 additions & 0 deletions bin/update-commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env node
'use strict'

require('dotenv').config()

const { REST } = require('@discordjs/rest')
const { Routes } = require('discord-api-types/v10')
const applicationCommands = require('../dist/interactions/data/application-commands')

async function updateCommands () {
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN)
const application = await rest.get(Routes.oauth2CurrentApplication())
await rest.put(Routes.applicationCommands(application.id), { body: Object.values(applicationCommands) })
}

updateCommands()
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
environment:
- POSTGRES_HOST=db
volumes:
- ./config/application.js:/opt/app/config/application.js
- ./application.js:/opt/app/dist/configs/application.js
command: /bin/bash ./bin/wait-for-it.sh db:5432 -- yarn start

db:
Expand Down
12 changes: 3 additions & 9 deletions ormconfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,9 @@ const baseConfig = {
port: 5432,
username: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
entities: [
'dist/entities/**/*.js'
],
migrations: [
'dist/migrations/**/*.js'
],
subscribers: [
'dist/subscribers/**/*.js'
],
entities: ['dist/entities/**/*.js'],
migrations: ['dist/migrations/**/*.js'],
subscribers: ['dist/subscribers/**/*.js'],
cli: {
entitiesDir: 'src/entities',
migrationsDir: 'src/migrations',
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"axios": "^0.26.0",
"class-validator": "^0.13.2",
"common-tags": "^1.8.2",
"discord.js": "^12.5.3",
"discord.js-commando": "^0.12.3",
"discord-api-types": "^0.33.0",
"discord.js": "^13.7.0",
"dotenv": "^16.0.0",
"emoji-regex": "^10.0.0",
"inversify": "^6.0.1",
"inversify-inject-decorators": "^3.1.0",
"lodash": "^4.17.21",
"node-cron": "^3.0.0",
"pg": "^8.7.1",
Expand All @@ -36,6 +36,7 @@
"ws": "^8.3.0"
},
"devDependencies": {
"@discordjs/rest": "^0.3.0",
"@guidojw/bloxy": "^5.7.6",
"@types/common-tags": "^1.8.1",
"@types/lodash": "^4.14.178",
Expand All @@ -54,7 +55,7 @@
"typescript": "^4.5.3"
},
"engines": {
"node": ">=14"
"node": ">=16.6.0"
},
"packageManager": "[email protected]"
}
13 changes: 13 additions & 0 deletions src/argument-types/always.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import BaseArgumentType from './base'
import { injectable } from 'inversify'

@injectable()
export default class AlwaysArgumentType extends BaseArgumentType<string> {
public validate (): boolean {
return true
}

public parse (value: string): string {
return value
}
}
111 changes: 111 additions & 0 deletions src/argument-types/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Collection, CommandInteraction } from 'discord.js'
import { type Constructor, constants } from '../utils'
import type { DataManager, GuildContextManager } from '../managers'
import type { GuildContext, IdentifiableStructure } from '../structures'
import { inject, injectable, named, unmanaged } from 'inversify'
import type { Argument } from '../interactions/application-commands'
import type { IdentifiableEntity } from '../entities'
import lodash from 'lodash'
import pluralize from 'pluralize'

const { TYPES } = constants

@injectable()
export default abstract class BaseArgumentType<T> {
public abstract validate (
value: string,
interaction: CommandInteraction,
arg: Argument<T>
): boolean | string | Promise<boolean | string>

public abstract parse (
value: string,
interaction: CommandInteraction,
arg: Argument<T>
): T | null | Promise<T | null>
}

export class BaseStructureArgumentType<
T extends IdentifiableStructure<number, U>,
U extends IdentifiableEntity
> extends BaseArgumentType<T> {
@inject(TYPES.Manager)
@named('GuildContextManager')
private readonly guildContexts!: GuildContextManager

protected readonly holds: Constructor<T>

private readonly managerName: string

public constructor (@unmanaged() holds: Constructor<T>, @unmanaged() managerName?: string) {
super()

this.holds = holds
this.managerName = typeof managerName === 'undefined'
? pluralize(lodash.camelCase(holds.name))
: managerName
}

public validate (
value: string,
interaction: CommandInteraction,
_arg: Argument<T>
): boolean | string | Promise<boolean | string> {
if (!interaction.inGuild()) {
return false
}
const context = this.guildContexts.resolve(interaction.guildId) as GuildContext

const manager = context[this.managerName as keyof typeof context] as unknown as DataManager<number, T, unknown, U>
const id = parseInt(value)
if (!isNaN(id)) {
const structure = manager.cache.get(id)
return typeof structure !== 'undefined' && structure instanceof this.holds
}
const search = value.toLowerCase()
const structures: Collection<number, T> = manager.cache.filter(this.filterInexact(search))
if (structures.size === 1) {
return true
}
const exactStructures = structures.filter(this.filterExact(search))
return exactStructures.size === 1
}

public parse (
value: string,
interaction: CommandInteraction,
_arg: Argument<T>
): T | null | Promise<T | null> {
if (!interaction.inGuild()) {
return null
}
const context = this.guildContexts.resolve(interaction.guildId) as GuildContext

const manager = context[this.managerName as keyof typeof context] as unknown as DataManager<number, T, unknown, U>
const id = parseInt(value)
if (!isNaN(id)) {
return manager.cache.get(id) ?? null
}
const search = value.toLowerCase()
const structures = manager.cache.filter(this.filterInexact(search))
if (structures.size === 0) {
return null
}
if (structures.size === 1) {
return structures.first() as T
}
const exactStructures = structures.filter(this.filterExact(search))
if (exactStructures.size === 1) {
return exactStructures.first() as T
}
return null
}

protected filterExact (search: string): (structure: T) => boolean {
return structure => structure instanceof this.holds && structure.toString().toLowerCase() === search
}

protected filterInexact (search: string): (structure: T) => boolean {
return structure => structure instanceof this.holds && structure.toString().toLowerCase().includes(search)
}
}
21 changes: 21 additions & 0 deletions src/argument-types/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import BaseArgumentType from './base'
import { injectable } from 'inversify'

@injectable()
export default class BooleanArgumentType extends BaseArgumentType<boolean> {
private readonly truthy = ['true', 't', 'yes', 'y', 'on', 'enable', 'enabled', '1', '+']
private readonly falsy = ['false', 'f', 'no', 'n', 'off', 'disable', 'disabled', '0', '-']

public validate (value: string): boolean {
const search = value.toLowerCase()
return this.truthy.includes(search) || this.falsy.includes(search)
}

public parse (value: string): boolean | null {
if (!this.validate(value)) {
return null
}
const search = value.toLowerCase()
return this.truthy.includes(search)
}
}
32 changes: 32 additions & 0 deletions src/argument-types/category-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CategoryChannel, type CommandInteraction } from 'discord.js'
import BaseArgumentType from './base'
import { injectable } from 'inversify'

@injectable()
export default class CategoryChannelArgumentType extends BaseArgumentType<CategoryChannel> {
public validate (value: string, interaction: CommandInteraction): boolean {
if (!interaction.inCachedGuild()) {
return false
}

const match = value.match(/(\d+)/)
if (match === null) {
return false
}
const channel = interaction.guild.channels.resolve(match[0])
return channel !== null && channel instanceof CategoryChannel
}

public parse (value: string, interaction: CommandInteraction): CategoryChannel | null {
if (!interaction.inCachedGuild()) {
return null
}

const match = value.match(/(\d+)/)
if (match === null) {
return null
}
const channel = interaction.guild.channels.resolve(match[0])
return channel instanceof CategoryChannel ? channel : null
}
}
11 changes: 11 additions & 0 deletions src/argument-types/channel-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseStructureArgumentType } from './base'
import { ChannelGroup } from '../structures'
import type { Group as GroupEntity } from '../entities'
import { injectable } from 'inversify'

@injectable()
export default class ChannelGroupArgumentType extends BaseStructureArgumentType<ChannelGroup, GroupEntity> {
public constructor () {
super(ChannelGroup, 'groups')
}
}
54 changes: 54 additions & 0 deletions src/argument-types/custom-emoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { CommandInteraction, GuildEmoji } from 'discord.js'
import BaseArgumentType from './base'
import { injectable } from 'inversify'

@injectable()
export default class CustomEmojiArgumentType extends BaseArgumentType<GuildEmoji> {
public validate (value: string, interaction: CommandInteraction): boolean {
const match = value.match(/^(?:<a?:([a-zA-Z0-9_]+):)?([0-9]+)>?$/)
if (match !== null && interaction.client.emojis.cache.has(match[2])) {
return true
}
if (!interaction.inCachedGuild()) {
return false
}
const search = value.toLowerCase()
const emojis = interaction.guild.emojis.cache.filter(this.filterInexact(search))
if (emojis.size === 1) {
return true
}
const exactEmojis = interaction.guild.emojis.cache.filter(this.filterExact(search))
return exactEmojis.size === 1
}

public parse (value: string, interaction: CommandInteraction): GuildEmoji | null {
const match = value.match(/^(?:<a?:([a-zA-Z0-9_]+):)?([0-9]+)>?$/)
if (match !== null) {
return interaction.client.emojis.cache.get(match[2]) ?? null
}
if (!interaction.inCachedGuild()) {
return null
}
const search = value.toLowerCase()
const emojis = interaction.guild.emojis.cache.filter(this.filterInexact(search))
if (emojis.size === 0) {
return null
}
if (emojis.size === 1) {
return emojis.first() as GuildEmoji
}
const exactEmojis = interaction.guild.emojis.cache.filter(this.filterExact(search))
if (exactEmojis.size === 1) {
return exactEmojis.first() as GuildEmoji
}
return null
}

private filterExact (search: string): (structure: GuildEmoji) => boolean {
return structure => structure.name?.toLowerCase() === search
}

private filterInexact (search: string): (structure: GuildEmoji) => boolean {
return structure => structure.name?.toLowerCase().includes(search) ?? false
}
}
19 changes: 19 additions & 0 deletions src/argument-types/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import BaseArgumentType from './base'
import { argumentUtil } from '../utils'
import { injectable } from 'inversify'

const { validDate } = argumentUtil

@injectable()
export default class DateArgumentType extends BaseArgumentType<string> {
public validate (value: string): boolean {
return validDate(value)
}

public parse (value: string): string | null {
if (!this.validate(value)) {
return null
}
return value
}
}
17 changes: 17 additions & 0 deletions src/argument-types/default-emoji.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import BaseArgumentType from './base'
import emojiRegex from 'emoji-regex'
import { injectable } from 'inversify'

@injectable()
export default class DefaultEmojiArgumentType extends BaseArgumentType<string> {
public validate (value: string): boolean {
return new RegExp(`^(?:${emojiRegex().source})$`).test(value)
}

public parse (value: string): string | null {
if (!this.validate(value)) {
return null
}
return value
}
}
11 changes: 11 additions & 0 deletions src/argument-types/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseStructureArgumentType } from './base'
import { Group } from '../structures'
import type { Group as GroupEntity } from '../entities'
import { injectable } from 'inversify'

@injectable()
export default class GroupArgumentType extends BaseStructureArgumentType<Group, GroupEntity> {
public constructor () {
super(Group)
}
}
Loading

0 comments on commit 42f4bc7

Please sign in to comment.