diff --git a/src/gateway/handlers/ready.ts b/src/gateway/handlers/ready.ts index 94c1123..5c928c5 100644 --- a/src/gateway/handlers/ready.ts +++ b/src/gateway/handlers/ready.ts @@ -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}`) diff --git a/src/managers/memberRoles.ts b/src/managers/memberRoles.ts index a287fa4..87cf9db 100644 --- a/src/managers/memberRoles.ts +++ b/src/managers/memberRoles.ts @@ -20,7 +20,7 @@ export class MemberRolesManager extends BaseChildManager< async get (id: string): Promise { 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 } diff --git a/src/managers/members.ts b/src/managers/members.ts index 45385fa..f3b6a9f 100644 --- a/src/managers/members.ts +++ b/src/managers/members.ts @@ -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 { guild: Guild @@ -14,11 +15,17 @@ export class MembersManager extends BaseManager { this.guild = guild } - async get (key: string): Promise { + async get(key: string): Promise { 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 { 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)) }) diff --git a/src/models/command.ts b/src/models/command.ts index b1d82bd..3d6d207 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -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) */ diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index cbe8945..de1ee06 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -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 }) } } } diff --git a/src/structures/guild.ts b/src/structures/guild.ts index e390024..19dab5a 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -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 { return (await this.roles.array().then(arr => arr?.sort((b, a) => a.position - b.position)[0]) as any) as Role } + + async me(): Promise { + 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 + } } diff --git a/src/structures/member.ts b/src/structures/member.ts index dc6d03e..3262643 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -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 { diff --git a/src/structures/role.ts b/src/structures/role.ts index 7c8b328..903fc38 100644 --- a/src/structures/role.ts +++ b/src/structures/role.ts @@ -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 } diff --git a/src/structures/user.ts b/src/structures/user.ts index da86e04..697e7c6 100644 --- a/src/structures/user.ts +++ b/src/structures/user.ts @@ -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 { diff --git a/src/test/cmds/userinfo.ts b/src/test/cmds/userinfo.ts index efbb062..04f6066 100644 --- a/src/test/cmds/userinfo.ts +++ b/src/test/cmds/userinfo.ts @@ -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) } diff --git a/src/types/permissionFlags.ts b/src/types/permissionFlags.ts index 9af057e..88b7b03 100644 --- a/src/types/permissionFlags.ts +++ b/src/types/permissionFlags.ts @@ -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, +} \ No newline at end of file diff --git a/src/types/userFlags.ts b/src/types/userFlags.ts new file mode 100644 index 0000000..5858104 --- /dev/null +++ b/src/types/userFlags.ts @@ -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 +} \ No newline at end of file diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts new file mode 100644 index 0000000..e1ee2ce --- /dev/null +++ b/src/utils/bitfield.ts @@ -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 { + 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 + } +} \ No newline at end of file diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts new file mode 100644 index 0000000..060a0c0 --- /dev/null +++ b/src/utils/permissions.ts @@ -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) + } +} \ No newline at end of file diff --git a/src/utils/userFlags.ts b/src/utils/userFlags.ts new file mode 100644 index 0000000..7be42f4 --- /dev/null +++ b/src/utils/userFlags.ts @@ -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) + } +} \ No newline at end of file