feat(commands): replace texts with events, add whitelisting

This commit is contained in:
DjDeveloperr 2020-11-15 13:02:46 +05:30
parent f6393c8226
commit 7835162fd5
15 changed files with 238 additions and 142 deletions

View file

@ -3,6 +3,7 @@ import { GuildPayload } from '../../types/guild.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
export const ready: GatewayEventHandler = async (gateway: Gateway, d: any) => {
await gateway.client.guilds.flush()
gateway.client.user = new User(gateway.client, d.user)
gateway.sessionID = d.session_id
gateway.debug(`Received READY. Session: ${gateway.sessionID}`)

View file

@ -20,7 +20,7 @@ export class MemberRolesManager extends BaseChildManager<
async get (id: string): Promise<Role | undefined> {
const res = await this.parent.get(id)
const mem = await (this.parent as any).guild.members._get(this.member.id) as MemberPayload
if (res !== undefined && mem.roles.includes(res.id) === true) return res
if (res !== undefined && (mem.roles.includes(res.id) === true || res.id === this.member.guild.id)) return res
else return undefined
}
@ -28,7 +28,7 @@ export class MemberRolesManager extends BaseChildManager<
const arr = (await this.parent.array()) as Role[]
const mem = await (this.parent as any).guild.members._get(this.member.id) as MemberPayload
return arr.filter(
(c: any) => mem.roles.includes(c.id)
(c: any) => mem.roles.includes(c.id) as boolean || c.id === this.member.guild.id
) as any
}

View file

@ -5,6 +5,7 @@ import { Member } from '../structures/member.ts'
import { GUILD_MEMBER } from '../types/endpoint.ts'
import { MemberPayload } from '../types/guild.ts'
import { BaseManager } from './base.ts'
import { Permissions } from "../utils/permissions.ts"
export class MembersManager extends BaseManager<MemberPayload, Member> {
guild: Guild
@ -14,11 +15,17 @@ export class MembersManager extends BaseManager<MemberPayload, Member> {
this.guild = guild
}
async get (key: string): Promise<Member | undefined> {
async get(key: string): Promise<Member | undefined> {
const raw = await this._get(key)
if (raw === undefined) return
const user = new User(this.client, raw.user)
const res = new this.DataType(this.client, raw, user, this.guild)
const roles = await this.guild.roles.array()
let permissions = new Permissions(Permissions.DEFAULT)
if (roles !== undefined) {
const mRoles = roles.filter(r => raw.roles.includes(r.id) as boolean || r.id === this.guild.id)
permissions = new Permissions(mRoles.map(r => r.permissions))
}
const res = new this.DataType(this.client, raw, user, this.guild, permissions)
return res
}
@ -27,7 +34,13 @@ export class MembersManager extends BaseManager<MemberPayload, Member> {
this.client.rest.get(GUILD_MEMBER(this.guild.id, id)).then(async data => {
await this.set(id, data as MemberPayload)
const user: User = new User(this.client, data.user)
const res = new Member(this.client, data as MemberPayload, user, this.guild)
const roles = await this.guild.roles.array()
let permissions = new Permissions(Permissions.DEFAULT)
if (roles !== undefined) {
const mRoles = roles.filter(r => data.roles.includes(r.id) as boolean || r.id === this.guild.id)
permissions = new Permissions(mRoles.map(r => r.permissions))
}
const res = new Member(this.client, data as MemberPayload, user, this.guild, permissions)
resolve(res)
}).catch(e => reject(e))
})

View file

@ -46,6 +46,16 @@ export class Command {
args?: number | boolean
/** Permission(s) required for using Command */
permissions?: string | string[]
/** Permission(s) bot will need in order to execute Command */
botPermissions?: string | string[]
/** Role(s) user will require in order to use Command. List or one of ID or name */
roles?: string | string[]
/** Whitelisted Guilds. Only these Guild(s) can execute Command. (List or one of IDs) */
whitelistedGuilds?: string | string[]
/** Whitelisted Channels. Command can be executed only in these channels. (List or one of IDs) */
whitelistedChannels?: string | string[]
/** Whitelisted Users. Command can be executed only by these Users (List or one of IDs) */
whitelistedUsers?: string | string[]
/** Whether the Command can only be used in Guild (if allowed in DMs) */
guildOnly?: boolean
/** Whether the Command can only be used in Bot's DMs (if allowed) */

View file

@ -1,5 +1,4 @@
import { Message } from "../../mod.ts"
import { Embed } from "../structures/embed.ts"
import { awaitSync } from "../utils/mixedPromise.ts"
import { Client, ClientOptions } from './client.ts'
import { CommandContext, CommandsManager, parseCommand } from './command.ts'
@ -23,33 +22,6 @@ export interface CommandClientOptions extends ClientOptions {
caseSensitive?: boolean
}
type CommandText = string | Embed
export interface CommandTexts {
GUILD_ONLY?: CommandText
OWNER_ONLY?: CommandText
DMS_ONLY?: CommandText
ERROR?: CommandText
}
export const DefaultCommandTexts: CommandTexts = {
GUILD_ONLY: 'This command can only be used in a Server!',
OWNER_ONLY: 'This command can only be used by Bot Owners!',
DMS_ONLY: "This command can only be used in Bot's DMs!",
ERROR: 'An error occured while executing command!'
}
interface Replaces {
[name: string]: string
}
export const massReplace = (text: string, replaces: Replaces): string => {
Object.entries(replaces).forEach(replace => {
text = text.replace(new RegExp(`{{${replace[0]}}}`, 'g'), replace[1])
})
return text
}
export class CommandClient extends Client implements CommandClientOptions {
prefix: string | string[]
mentionPrefix: boolean
@ -66,7 +38,6 @@ export class CommandClient extends Client implements CommandClientOptions {
caseSensitive: boolean
extensions: ExtensionsManager = new ExtensionsManager(this)
commands: CommandsManager = new CommandsManager(this)
texts: CommandTexts = DefaultCommandTexts
constructor(options: CommandClientOptions) {
super(options)
@ -162,31 +133,9 @@ export class CommandClient extends Client implements CommandClientOptions {
if (command === undefined) return
const baseReplaces: Replaces = {
command: command.name,
nameUsed: parsed.name,
prefix,
username: msg.author.username,
tag: msg.author.tag,
mention: msg.author.mention,
id: msg.author.id
}
if (command.guildOnly === true && msg.guild === undefined) {
if (this.texts.GUILD_ONLY !== undefined)
return this.sendProcessedText(msg, this.texts.GUILD_ONLY, baseReplaces)
return
}
if (command.dmOnly === true && msg.guild !== undefined) {
if (this.texts.DMS_ONLY !== undefined)
return this.sendProcessedText(msg, this.texts.DMS_ONLY, baseReplaces)
return
}
if (command.ownerOnly === true && !this.owners.includes(msg.author.id)) {
if (this.texts.OWNER_ONLY !== undefined)
return this.sendProcessedText(msg, this.texts.OWNER_ONLY, baseReplaces)
return
}
if (command.whitelistedGuilds !== undefined && msg.guild !== undefined && command.whitelistedGuilds.includes(msg.guild.id) === false) return;
if (command.whitelistedChannels !== undefined && command.whitelistedChannels.includes(msg.channel.id) === false) return;
if (command.whitelistedUsers !== undefined && command.whitelistedUsers.includes(msg.author.id) === false) return;
const ctx: CommandContext = {
client: this,
@ -201,6 +150,22 @@ export class CommandClient extends Client implements CommandClientOptions {
guild: msg.guild
}
if (command.guildOnly === true && msg.guild === undefined) return this.emit('commandGuildOnly', { ctx, command })
if (command.dmOnly === true && msg.guild !== undefined) return this.emit('commandDmOnly', { ctx, command })
if (command.ownerOnly === true && !this.owners.includes(msg.author.id)) return this.emit('commandOwnerOnly', { ctx, command })
if (command.permissions !== undefined && msg.guild !== undefined) {
const missing: string[] = []
let perms: string[] = []
if (typeof command.permissions === 'string') perms = [command.permissions]
else perms = command.permissions
for (const perm of perms) {
const has = msg.member?.permissions.has(perm)
if (has !== true) missing.push(perm)
}
if (missing.length !== 0) return this.emit('commandMissingPermissions', { command, missing, ctx })
}
try {
this.emit('commandUsed', { context: ctx })
@ -210,30 +175,7 @@ export class CommandClient extends Client implements CommandClientOptions {
const result = await awaitSync(command.execute(ctx))
command.afterExecute(ctx, result)
} catch (e) {
if (this.texts.ERROR !== undefined)
this.sendProcessedText(
msg,
this.texts.ERROR,
Object.assign(baseReplaces, { error: e.message })
)
this.emit('commandError', { command, parsed, error: e })
}
}
sendProcessedText(msg: Message, text: CommandText, replaces: Replaces): any {
if (typeof text === 'string') {
text = massReplace(text, replaces)
return msg.channel.send(text)
} else {
if (text.description !== undefined)
text.description = massReplace(text.description, replaces)
if (text.title !== undefined)
text.description = massReplace(text.title, replaces)
if (text.author?.name !== undefined)
text.description = massReplace(text.author.name, replaces)
if (text.footer?.text !== undefined)
text.description = massReplace(text.footer.text, replaces)
return msg.channel.send(text)
this.emit('commandError', { command, parsed, error: e, ctx })
}
}
}

View file

@ -8,6 +8,7 @@ import { GuildChannelsManager } from '../managers/guildChannels.ts'
import { MembersManager } from '../managers/members.ts'
import { Role } from './role.ts'
import { GuildEmojisManager } from '../managers/guildEmojis.ts'
import { Member } from "./member.ts"
export class Guild extends Base {
id: string
@ -214,4 +215,10 @@ export class Guild extends Base {
async getEveryoneRole (): Promise<Role> {
return (await this.roles.array().then(arr => arr?.sort((b, a) => a.position - b.position)[0]) as any) as Role
}
async me(): Promise<Member> {
const get = await this.members.get(this.client.user?.id as string)
if (get === undefined) throw new Error('Guild#me is not cached')
return get
}
}

View file

@ -1,6 +1,7 @@
import { MemberRolesManager } from "../managers/memberRoles.ts"
import { Client } from '../models/client.ts'
import { MemberPayload } from '../types/guild.ts'
import { Permissions } from "../utils/permissions.ts"
import { Base } from './base.ts'
import { Guild } from "./guild.ts"
import { User } from './user.ts'
@ -15,13 +16,12 @@ export class Member extends Base {
deaf: boolean
mute: boolean
guild: Guild
permissions: Permissions
constructor (client: Client, data: MemberPayload, user: User, guild: Guild) {
constructor (client: Client, data: MemberPayload, user: User, guild: Guild, perms?: Permissions) {
super(client)
this.id = data.user.id
this.user = user
// this.user =
// cache.get('user', data.user.id) ?? new User(this.client, data.user)
this.nick = data.nick
this.guild = guild
this.roles = new MemberRolesManager(this.client, this.guild.roles, this)
@ -29,8 +29,8 @@ export class Member extends Base {
this.premiumSince = data.premium_since
this.deaf = data.deaf
this.mute = data.mute
// TODO: Cache in Gateway Event Code
// cache.set('member', this.id, this)
if (perms !== undefined) this.permissions = perms
else this.permissions = new Permissions(Permissions.DEFAULT)
}
protected readFromData (data: MemberPayload): void {

View file

@ -1,6 +1,7 @@
import { Client } from '../models/client.ts'
import { Base } from './base.ts'
import { RolePayload } from '../types/role.ts'
import { Permissions } from "../utils/permissions.ts"
export class Role extends Base {
id: string
@ -8,7 +9,7 @@ export class Role extends Base {
color: number
hoist: boolean
position: number
permissions: string
permissions: Permissions
managed: boolean
mentionable: boolean
@ -25,11 +26,9 @@ export class Role extends Base {
this.color = data.color
this.hoist = data.hoist
this.position = data.position
this.permissions = data.permissions
this.permissions = new Permissions(data.permissions)
this.managed = data.managed
this.mentionable = data.mentionable
// TODO: Cache in Gateway Event Code
// cache.set('role', this.id, this)
}
protected readFromData (data: RolePayload): void {
@ -38,7 +37,7 @@ export class Role extends Base {
this.color = data.color ?? this.color
this.hoist = data.hoist ?? this.hoist
this.position = data.position ?? this.position
this.permissions = data.permissions ?? this.permissions
this.permissions = new Permissions(data.permissions) ?? this.permissions
this.managed = data.managed ?? this.managed
this.mentionable = data.mentionable ?? this.mentionable
}

View file

@ -1,5 +1,6 @@
import { Client } from '../models/client.ts'
import { UserPayload } from '../types/user.ts'
import { UserFlagsManager } from "../utils/userFlags.ts"
import { Base } from './base.ts'
export class User extends Base {
@ -13,9 +14,9 @@ export class User extends Base {
locale?: string
verified?: boolean
email?: string
flags?: number
flags?: UserFlagsManager
premiumType?: 0 | 1 | 2
publicFlags?: number
publicFlags?: UserFlagsManager
get tag (): string {
return `${this.username}#${this.discriminator}`
@ -41,11 +42,9 @@ export class User extends Base {
this.locale = data.locale
this.verified = data.verified
this.email = data.email
this.flags = data.flags
this.flags = new UserFlagsManager(data.flags)
this.premiumType = data.premium_type
this.publicFlags = data.public_flags
// TODO: Cache in Gateway Event Code
// cache.set('user', this.id, this)
this.publicFlags = new UserFlagsManager(data.public_flags)
}
protected readFromData (data: UserPayload): void {
@ -59,9 +58,9 @@ export class User extends Base {
this.locale = data.locale ?? this.locale
this.verified = data.verified ?? this.verified
this.email = data.email ?? this.email
this.flags = data.flags ?? this.flags
this.flags = new UserFlagsManager(data.flags) ?? this.flags
this.premiumType = data.premium_type ?? this.premiumType
this.publicFlags = data.public_flags ?? this.publicFlags
this.publicFlags = new UserFlagsManager(data.public_flags) ?? this.publicFlags
}
toString (): string {

View file

@ -13,6 +13,7 @@ export default class UserinfoCommand extends Command {
.setAuthor({ name: member.user.tag })
.addField("ID", member.id)
.addField("Roles", roles.map(r => r.name).join(", "))
.addField('Permissions', JSON.stringify(member.permissions.has('ADMINISTRATOR')))
.setColor(0xff00ff)
ctx.channel.send(embed)
}

View file

@ -1,43 +1,35 @@
// https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
enum PermissionFlags {
CREATE_INSTANT_INVITE = 0x00000001,
KICK_MEMBERS = 0x00000002,
BAN_MEMBERS = 0x00000004,
ADMINISTRATOR = 0x00000008,
MANAGE_CHANNELS = 0x00000010,
MANAGE_GUILD = 0x00000020,
ADD_REACTIONS = 0x00000040,
VIEW_AUDIT_LOG = 0x00000080,
PRIORITY_SPEAKER = 0x00000100,
STREAM = 0x00000200,
VIEW_CHANNEL = 0x00000400,
SEND_MESSAGES = 0x00000800,
SEND_TTS_MESSAGES = 0x00001000,
MANAGE_MESSAGES = 0x00002000,
EMBED_LINKS = 0x00004000,
ATTACH_FILES = 0x00008000,
READ_MESSAGE_HISTORY = 0x00010000,
MENTION_EVERYONE = 0x00020000,
USE_EXTERNAL_EMOJIS = 0x00040000,
VIEW_GUILD_INSIGHTS = 0x00080000,
CONNECT = 0x00100000,
SPEAK = 0x00200000,
MUTE_MEMBERS = 0x00400000,
DEAFEN_MEMBERS = 0x00800000,
MOVE_MEMBERS = 0x01000000,
USE_VAD = 0x02000000,
CHANGE_NICKNAME = 0x04000000,
MANAGE_NICKNAMES = 0x08000000,
MANAGE_ROLES = 0x10000000,
MANAGE_WEBHOOKS = 0x20000000,
MANAGE_EMOJIS = 0x40000000
}
export { PermissionFlags }
export const PermissionFlags: { [key: string]: number } = {
CREATE_INSTANT_INVITE: 1 << 0,
KICK_MEMBERS: 1 << 1,
BAN_MEMBERS: 1 << 2,
ADMINISTRATOR: 1 << 3,
MANAGE_CHANNELS: 1 << 4,
MANAGE_GUILD: 1 << 5,
ADD_REACTIONS: 1 << 6,
VIEW_AUDIT_LOG: 1 << 7,
PRIORITY_SPEAKER: 1 << 8,
STREAM: 1 << 9,
VIEW_CHANNEL: 1 << 10,
SEND_MESSAGES: 1 << 11,
SEND_TTS_MESSAGES: 1 << 12,
MANAGE_MESSAGES: 1 << 13,
EMBED_LINKS: 1 << 14,
ATTACH_FILES: 1 << 15,
READ_MESSAGE_HISTORY: 1 << 16,
MENTION_EVERYONE: 1 << 17,
USE_EXTERNAL_EMOJIS: 1 << 18,
VIEW_GUILD_INSIGHTS: 1 << 19,
CONNECT: 1 << 20,
SPEAK: 1 << 21,
MUTE_MEMBERS: 1 << 22,
DEAFEN_MEMBERS: 1 << 23,
MOVE_MEMBERS: 1 << 24,
USE_VAD: 1 << 25,
CHANGE_NICKNAME: 1 << 26,
MANAGE_NICKNAMES: 1 << 27,
MANAGE_ROLES: 1 << 28,
MANAGE_WEBHOOKS: 1 << 29,
MANAGE_EMOJIS: 1 << 30,
}

16
src/types/userFlags.ts Normal file
View file

@ -0,0 +1,16 @@
export const UserFlags = {
DISCORD_EMPLOYEE: 1 << 0,
PARTNERED_SERVER_OWNER: 1 << 1,
DISCORD_PARTNER: 1 << 1,
HYPESQUAD_EVENTS: 1 << 2,
BUGHUNTER_LEVEL_1: 1 << 3,
HOUSE_BRAVERY: 1 << 6,
HOUSE_BRILLIANCE: 1 << 7,
HOUSE_BALANCE: 1 << 8,
EARLY_SUPPORTER: 1 << 9,
TEAM_USER: 1 << 10,
SYSTEM: 1 << 12,
BUGHUNTER_LEVEL_2: 1 << 14,
VERIFIED_BOT: 1 << 16,
EARLY_VERIFIED_DEVELOPER: 1 << 17
}

86
src/utils/bitfield.ts Normal file
View file

@ -0,0 +1,86 @@
// Ported from https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js
export type BitFieldResolvable = number | BitField | string | BitField[]
export class BitField {
flags: { [name: string]: number } = {}
bitfield: any
constructor(flags: { [name: string]: number }, bits: any) {
this.flags = flags
this.bitfield = BitField.resolve(this.flags, bits)
}
any(bit: BitFieldResolvable): boolean {
return (this.bitfield & BitField.resolve(this.flags, bit)) !== 0
}
equals(bit: BitFieldResolvable): boolean {
return this.bitfield === BitField.resolve(this.flags, bit)
}
has(bit: BitFieldResolvable, ...args: any[]): boolean {
if (Array.isArray(bit)) return bit.every(p => this.has(p))
return (this.bitfield & BitField.resolve(this.flags, bit)) === bit
}
missing(bits: any, ...hasParams: any[]): string[] {
if (!Array.isArray(bits)) bits = new BitField(this.flags, bits).toArray(false)
return bits.filter((p: any) => !this.has(p, ...hasParams))
}
freeze(): Readonly<BitField> {
return Object.freeze(this)
}
add(...bits: BitFieldResolvable[]): BitField {
let total = 0
for (const bit of bits) {
total |= BitField.resolve(this.flags, bit)
}
if (Object.isFrozen(this)) return new BitField(this.flags, this.bitfield | total)
this.bitfield |= total
return this
}
remove(...bits: BitFieldResolvable[]): BitField {
let total = 0
for (const bit of bits) {
total |= BitField.resolve(this.flags, bit)
}
if (Object.isFrozen(this)) return new BitField(this.flags, this.bitfield & ~total)
this.bitfield &= ~total
return this
}
serialize(...hasParams: any[]): { [key: string]: any } {
const serialized: { [key: string]: any } = {}
for (const [flag, bit] of Object.entries(this.flags)) serialized[flag] = this.has(BitField.resolve(this.flags, bit), ...hasParams)
return serialized
}
toArray(...hasParams: any[]): string[] {
return Object.keys(this.flags).filter(bit => this.has(BitField.resolve(this.flags, bit), ...hasParams))
}
toJSON(): any {
return this.bitfield
}
valueOf(): any {
return this.bitfield
}
*[Symbol.iterator](): any {
yield* this.toArray()
}
static resolve(flags: any, bit: BitFieldResolvable = 0): number {
if (typeof bit === 'string' && !isNaN(parseInt(bit))) return parseInt(bit)
if (typeof bit === 'number' && bit >= 0) return bit
if (bit instanceof BitField) return this.resolve(flags, bit.bitfield)
if (Array.isArray(bit)) return bit.map(p => this.resolve(flags, p)).reduce((prev, p) => prev | p, 0)
if (typeof bit === 'string' && typeof flags[bit] !== 'undefined') return flags[bit]
const error = new RangeError('BITFIELD_INVALID')
throw error
}
}

22
src/utils/permissions.ts Normal file
View file

@ -0,0 +1,22 @@
// Ported from https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js
import { PermissionFlags } from "../types/permissionFlags.ts"
import { BitField } from "./bitfield.ts"
export type PermissionResolvable = string | number | Permissions | PermissionResolvable[]
export class Permissions extends BitField {
static DEFAULT = 104324673
static ALL = Object.values(PermissionFlags).reduce((all, p) => all | p, 0)
constructor(bits: any) {
super(PermissionFlags, bits)
}
any(permission: PermissionResolvable, checkAdmin = true): boolean {
return (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || super.any(permission as any)
}
has(permission: PermissionResolvable, checkAdmin = true): boolean {
return (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || super.has(permission as any)
}
}

8
src/utils/userFlags.ts Normal file
View file

@ -0,0 +1,8 @@
import { UserFlags } from "../types/userFlags.ts";
import { BitField } from "./bitfield.ts";
export class UserFlagsManager extends BitField {
constructor(bits: any) {
super(UserFlags, bits)
}
}