Merge pull request #36 from DjDeveloperr/main

implement: new Gateway Events, additions to Command Client, message caching
This commit is contained in:
DjDeveloper 2020-11-16 17:17:47 +05:30 committed by GitHub
commit c80f9dc1a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 741 additions and 204 deletions

View file

@ -1,12 +1,14 @@
import { Gateway, GatewayEventHandler } from '../index.ts' import { Gateway, GatewayEventHandler } from '../index.ts'
import getChannelByType from '../../utils/getChannelByType.ts' import getChannelByType from '../../utils/getChannelByType.ts'
import { ChannelPayload } from '../../types/channel.ts' import { ChannelPayload } from '../../types/channel.ts'
import { Guild } from "../../structures/guild.ts"
export const channelCreate: GatewayEventHandler = async ( export const channelCreate: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,
d: ChannelPayload d: ChannelPayload
) => { ) => {
const channel = getChannelByType(gateway.client, d) const guild: undefined | Guild = (d as any).guild_id !== undefined ? await gateway.client.guilds.get((d as any).guild_id) : undefined
const channel = getChannelByType(gateway.client, d, guild)
if (channel !== undefined) { if (channel !== undefined) {
await gateway.client.channels.set(d.id, d) await gateway.client.channels.set(d.id, d)
gateway.client.emit('channelCreate', channel) gateway.client.emit('channelCreate', channel)

View file

@ -11,7 +11,7 @@ export const guildBanAdd: GatewayEventHandler = async (
const user: User = await gateway.client.users.get(d.user.id) ?? new User(gateway.client, d.user) const user: User = await gateway.client.users.get(d.user.id) ?? new User(gateway.client, d.user)
if (guild !== undefined) { if (guild !== undefined) {
await guild.members.delete(user.id) // We don't have to delete member, already done with guildMemberRemove event
gateway.client.emit('guildBanAdd', guild, user) gateway.client.emit('guildBanAdd', guild, user)
} }
} }

View file

@ -1,14 +1,53 @@
import cache from '../../models/cache.ts' import { Emoji } from "../../structures/emoji.ts"
import { Guild } from '../../structures/guild.ts' import { Guild } from '../../structures/guild.ts'
import { EmojiPayload } from "../../types/emoji.ts"
import { GuildEmojiUpdatePayload } from '../../types/gateway.ts' import { GuildEmojiUpdatePayload } from '../../types/gateway.ts'
import { Gateway, GatewayEventHandler } from '../index.ts' import { Gateway, GatewayEventHandler } from '../index.ts'
export const guildEmojiUpdate: GatewayEventHandler = ( export const guildEmojiUpdate: GatewayEventHandler = async (
gateway: Gateway, gateway: Gateway,
d: GuildEmojiUpdatePayload d: GuildEmojiUpdatePayload
) => { ) => {
const guild: Guild = cache.get('guild', d.guild_id) const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
if (guild !== undefined) { if (guild !== undefined) {
// const emojis = guild.emojis const emojis = await guild.emojis.collection()
const deleted: Emoji[] = []
const added: Emoji[] = []
const updated: Array<{ before: Emoji, after: Emoji }> = []
const _updated: EmojiPayload[] = []
for (const raw of d.emojis) {
const has = emojis.get(raw.id)
if (has === undefined) {
await guild.emojis.set(raw.id, raw)
const emoji = await guild.emojis.get(raw.id) as Emoji
added.push(emoji)
} else _updated.push(raw)
}
for (const emoji of emojis.values()) {
const find = _updated.find(e => emoji.id === e.id)
if (find === undefined) {
await guild.emojis.delete(emoji.id)
deleted.push(emoji)
} else {
const before = await guild.emojis.get(find.id) as Emoji
await guild.emojis.set(find.id, find)
const after = await guild.emojis.get(find.id) as Emoji
updated.push({ before, after })
}
}
for (const emoji of deleted) {
gateway.client.emit('guildEmojiDelete', emoji)
}
for (const emoji of added) {
gateway.client.emit('guildEmojiAdd', emoji)
}
for (const emoji of updated) {
gateway.client.emit('guildEmojiUpdate', emoji.before, emoji.after)
}
} }
} }

View file

@ -0,0 +1,15 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
export const guildMemberAdd: GatewayEventHandler = async (
gateway: Gateway,
d: any
) => {
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
// Weird case, shouldn't happen
if (guild === undefined) return
await guild.members.set(d.id, d)
const member = await guild.members.get(d.id)
gateway.client.emit('guildMemberAdd', member)
}

View file

@ -0,0 +1,21 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
import { User } from "../../structures/user.ts"
export const guildMemberRemove: GatewayEventHandler = async (
gateway: Gateway,
d: any
) => {
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
// Weird case, shouldn't happen
if (guild === undefined) return
const member = await guild.members.get(d.id)
await guild.members.delete(d.id)
if (member !== undefined) gateway.client.emit('guildMemberRemove', member)
else {
const user = new User(gateway.client, d.user)
gateway.client.emit('guildMemberRemoveUncached', user)
}
}

View file

@ -0,0 +1,20 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
export const guildMemberUpdate: GatewayEventHandler = async (
gateway: Gateway,
d: any
) => {
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
// Weird case, shouldn't happen
if (guild === undefined) return
const member = await guild.members.get(d.id)
await guild.members.set(d.id, d)
const newMember = await guild.members.get(d.id)
if (member !== undefined) gateway.client.emit('guildMemberRemove', member, newMember)
else {
gateway.client.emit('guildMemberUpdateUncached', newMember)
}
}

View file

@ -0,0 +1,16 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
import { GuildRoleCreatePayload } from "../../types/gateway.ts"
export const guildRoleCreate: GatewayEventHandler = async (
gateway: Gateway,
d: GuildRoleCreatePayload
) => {
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
// Weird case, shouldn't happen
if (guild === undefined) return
await guild.roles.set(d.role.id, d.role)
const role = await guild.roles.get(d.role.id)
gateway.client.emit('guildRoleCreate', role)
}

View file

@ -0,0 +1,18 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
import { GuildRoleDeletePayload } from "../../types/gateway.ts"
export const guildRoleDelete: GatewayEventHandler = async (
gateway: Gateway,
d: GuildRoleDeletePayload
) => {
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
// Weird case, shouldn't happen
if (guild === undefined) return
const role = await guild.roles.get(d.role_id)
// Shouldn't happen either
if(role === undefined) return
gateway.client.emit('guildRoleDelete', role)
}

View file

@ -0,0 +1,21 @@
import { Gateway, GatewayEventHandler } from '../index.ts'
import { Guild } from '../../structures/guild.ts'
import { GuildRoleUpdatePayload } from "../../types/gateway.ts"
export const guildRoleUpdate: GatewayEventHandler = async (
gateway: Gateway,
d: GuildRoleUpdatePayload
) => {
const guild: Guild | undefined = await gateway.client.guilds.get(d.guild_id)
// Weird case, shouldn't happen
if (guild === undefined) return
const role = await guild.roles.get(d.role.id)
await guild.roles.set(d.role.id, d.role)
const newRole = await guild.roles.get(d.role.id)
// Shouldn't happen either
if(role === undefined) return gateway.client.emit('guildRoleUpdateUncached', newRole)
gateway.client.emit('guildRoleUpdate', role, newRole)
}

View file

@ -13,6 +13,15 @@ import { guildBanRemove } from './guildBanRemove.ts'
import { messageCreate } from './messageCreate.ts' import { messageCreate } from './messageCreate.ts'
import { resume } from './resume.ts' import { resume } from './resume.ts'
import { reconnect } from './reconnect.ts' import { reconnect } from './reconnect.ts'
import { messageDelete } from "./messageDelete.ts"
import { messageUpdate } from "./messageUpdate.ts"
import { guildEmojiUpdate } from "./guildEmojiUpdate.ts"
import { guildMemberAdd } from "./guildMemberAdd.ts"
import { guildMemberRemove } from "./guildMemberRemove.ts"
import { guildMemberUpdate } from "./guildMemberUpdate.ts"
import { guildRoleCreate } from "./guildRoleCreate.ts"
import { guildRoleDelete } from "./guildRoleDelete.ts"
import { guildRoleUpdate } from "./guildRoleUpdate.ts"
export const gatewayHandlers: { export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined [eventCode in GatewayEvents]: GatewayEventHandler | undefined
@ -29,20 +38,20 @@ export const gatewayHandlers: {
GUILD_UPDATE: guildUpdate, GUILD_UPDATE: guildUpdate,
GUILD_BAN_ADD: guildBanAdd, GUILD_BAN_ADD: guildBanAdd,
GUILD_BAN_REMOVE: guildBanRemove, GUILD_BAN_REMOVE: guildBanRemove,
GUILD_EMOJIS_UPDATE: undefined, GUILD_EMOJIS_UPDATE: guildEmojiUpdate,
GUILD_INTEGRATIONS_UPDATE: undefined, GUILD_INTEGRATIONS_UPDATE: undefined,
GUILD_MEMBER_ADD: undefined, GUILD_MEMBER_ADD: guildMemberAdd,
GUILD_MEMBER_REMOVE: undefined, GUILD_MEMBER_REMOVE: guildMemberRemove,
GUILD_MEMBER_UPDATE: undefined, GUILD_MEMBER_UPDATE: guildMemberUpdate,
GUILD_MEMBERS_CHUNK: undefined, GUILD_MEMBERS_CHUNK: undefined,
GUILD_ROLE_CREATE: undefined, GUILD_ROLE_CREATE: guildRoleCreate,
GUILD_ROLE_UPDATE: undefined, GUILD_ROLE_UPDATE: guildRoleUpdate,
GUILD_ROLE_DELETE: undefined, GUILD_ROLE_DELETE: guildRoleDelete,
INVITE_CREATE: undefined, INVITE_CREATE: undefined,
INVITE_DELETE: undefined, INVITE_DELETE: undefined,
MESSAGE_CREATE: messageCreate, MESSAGE_CREATE: messageCreate,
MESSAGE_UPDATE: undefined, MESSAGE_UPDATE: messageUpdate,
MESSAGE_DELETE: undefined, MESSAGE_DELETE: messageDelete,
MESSAGE_DELETE_BULK: undefined, MESSAGE_DELETE_BULK: undefined,
MESSAGE_REACTION_ADD: undefined, MESSAGE_REACTION_ADD: undefined,
MESSAGE_REACTION_REMOVE: undefined, MESSAGE_REACTION_REMOVE: undefined,

View file

@ -13,6 +13,7 @@ export const messageCreate: GatewayEventHandler = async (
if (channel === undefined) if (channel === undefined)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel
await channel.messages.set(d.id, d)
const user = new User(gateway.client, d.author) const user = new User(gateway.client, d.author)
await gateway.client.users.set(d.author.id, d.author) await gateway.client.users.set(d.author.id, d.author)
let guild let guild

View file

@ -0,0 +1,19 @@
import { TextChannel } from '../../structures/textChannel.ts'
import { MessageDeletePayload } from "../../types/gateway.ts"
import { Gateway, GatewayEventHandler } from '../index.ts'
export const messageDelete: GatewayEventHandler = async (
gateway: Gateway,
d: MessageDeletePayload
) => {
let channel = await gateway.client.channels.get<TextChannel>(d.channel_id)
// Fetch the channel if not cached
if (channel === undefined)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel
const message = await channel.messages.get(d.id)
if (message === undefined) return gateway.client.emit('messageDeleteUncached', d)
await channel.messages.delete(d.id)
gateway.client.emit('messageDelete', message)
}

View file

@ -0,0 +1,25 @@
import { Message } from "../../structures/message.ts"
import { TextChannel } from '../../structures/textChannel.ts'
import { User } from "../../structures/user.ts"
import { Gateway, GatewayEventHandler } from '../index.ts'
export const messageUpdate: GatewayEventHandler = async (
gateway: Gateway,
d: any
) => {
let channel = await gateway.client.channels.get<TextChannel>(d.channel_id)
// Fetch the channel if not cached
if (channel === undefined)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel
const message = await channel.messages.get(d.id)
const author = message?.author !== undefined ? message.author : new User(gateway.client, d)
const newMsg = new Message(gateway.client, d, channel, author)
if (message === undefined) {
await channel.messages.set(d.id, d)
return gateway.client.emit('messageUpdateUncached', newMsg)
}
await channel.messages.set(d.id, d)
gateway.client.emit('messageUpdate', message, newMsg)
}

View file

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

View file

@ -115,6 +115,8 @@ class Gateway {
await this.cache.set('seq', s) await this.cache.set('seq', s)
} }
if (t !== null && t !== undefined) { if (t !== null && t !== undefined) {
this.client.emit('raw', t, d)
const handler = gatewayHandlers[t] const handler = gatewayHandlers[t]
if (handler !== undefined) { if (handler !== undefined) {

View file

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

View file

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

View file

@ -7,8 +7,11 @@ import { CHANNEL_MESSAGE } from '../types/endpoint.ts'
import { BaseManager } from './base.ts' import { BaseManager } from './base.ts'
export class MessagesManager extends BaseManager<MessagePayload, Message> { export class MessagesManager extends BaseManager<MessagePayload, Message> {
constructor (client: Client) { channel: TextChannel
constructor (client: Client, channel: TextChannel) {
super(client, 'messages', Message) super(client, 'messages', Message)
this.channel = channel
} }
async get (key: string): Promise<Message | undefined> { async get (key: string): Promise<Message | undefined> {
@ -26,18 +29,22 @@ export class MessagesManager extends BaseManager<MessagePayload, Message> {
return res return res
} }
async fetch (channelID: string, id: string): Promise<Message> { async set (key: string, value: MessagePayload): Promise<any> {
return this.client.cache.set(this.cacheName, key, value, this.client.messageCacheLifetime)
}
async fetch (id: string): Promise<Message> {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
this.client.rest this.client.rest
.get(CHANNEL_MESSAGE(channelID, id)) .get(CHANNEL_MESSAGE(this.channel.id, id))
.then(async data => { .then(async data => {
this.set(id, data as MessagePayload) await this.set(id, data as MessagePayload)
let channel: any = await this.client.channels.get<TextChannel>( let channel: any = await this.client.channels.get<TextChannel>(
channelID this.channel.id
) )
if (channel === undefined) if (channel === undefined)
channel = await this.client.channels.fetch(channelID) channel = await this.client.channels.fetch(this.channel.id)
const author = new User(this.client, (data as MessagePayload).author) const author = new User(this.client, (data as MessagePayload).author)
await this.client.users.set( await this.client.users.set(

View file

@ -7,7 +7,7 @@ import {
export interface ICacheAdapter { export interface ICacheAdapter {
get: (cacheName: string, key: string) => Promise<any> | any get: (cacheName: string, key: string) => Promise<any> | any
set: (cacheName: string, key: string, value: any) => Promise<any> | any set: (cacheName: string, key: string, value: any, expire?: number) => Promise<any> | any
delete: (cacheName: string, key: string) => Promise<boolean> | boolean delete: (cacheName: string, key: string) => Promise<boolean> | boolean
array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined> array: (cacheName: string) => undefined | any[] | Promise<any[] | undefined>
deleteCache: (cacheName: string) => any deleteCache: (cacheName: string) => any
@ -18,28 +18,31 @@ export class DefaultCacheAdapter implements ICacheAdapter {
[name: string]: Collection<string, any> [name: string]: Collection<string, any>
} = {} } = {}
async get (cacheName: string, key: string): Promise<undefined | any> { async get(cacheName: string, key: string): Promise<undefined | any> {
const cache = this.data[cacheName] const cache = this.data[cacheName]
if (cache === undefined) return if (cache === undefined) return
return cache.get(key) return cache.get(key)
} }
async set (cacheName: string, key: string, value: any): Promise<any> { async set(cacheName: string, key: string, value: any, expire?: number): Promise<any> {
let cache = this.data[cacheName] let cache = this.data[cacheName]
if (cache === undefined) { if (cache === undefined) {
this.data[cacheName] = new Collection() this.data[cacheName] = new Collection()
cache = this.data[cacheName] cache = this.data[cacheName]
} }
return cache.set(key, value) cache.set(key, value)
if (expire !== undefined) setTimeout(() => {
cache.delete(key)
}, expire)
} }
async delete (cacheName: string, key: string): Promise<boolean> { async delete(cacheName: string, key: string): Promise<boolean> {
const cache = this.data[cacheName] const cache = this.data[cacheName]
if (cache === undefined) return false if (cache === undefined) return false
return cache.delete(key) return cache.delete(key)
} }
async array (cacheName: string): Promise<any[] | undefined> { async array(cacheName: string): Promise<any[] | undefined> {
const cache = this.data[cacheName] const cache = this.data[cacheName]
if (cache === undefined) return if (cache === undefined) return
return cache.array() return cache.array()
@ -55,13 +58,16 @@ export class RedisCacheAdapter implements ICacheAdapter {
_redis: Promise<Redis> _redis: Promise<Redis>
redis?: Redis redis?: Redis
ready: boolean = false ready: boolean = false
readonly _expireIntervalTimer: number = 5000
private _expireInterval?: number
constructor (options: RedisConnectOptions) { constructor(options: RedisConnectOptions) {
this._redis = connect(options) this._redis = connect(options)
this._redis.then( this._redis.then(
redis => { redis => {
this.redis = redis this.redis = redis
this.ready = true this.ready = true
this._startExpireInterval()
}, },
() => { () => {
// TODO: Make error for this // TODO: Make error for this
@ -69,11 +75,31 @@ export class RedisCacheAdapter implements ICacheAdapter {
) )
} }
async _checkReady (): Promise<void> { private _startExpireInterval(): void {
this._expireInterval = setInterval(() => {
this.redis?.scan(0, { pattern: '*:expires' }).then(([_, names]) => {
for (const name of names) {
this.redis?.hvals(name).then(vals => {
for (const val of vals) {
const expireVal: {
name: string,
key: string,
at: number
} = JSON.parse(val)
const expired = new Date().getTime() > expireVal.at
if (expired) this.redis?.hdel(expireVal.name, expireVal.key)
}
})
}
})
}, this._expireIntervalTimer)
}
async _checkReady(): Promise<void> {
if (!this.ready) await this._redis if (!this.ready) await this._redis
} }
async get (cacheName: string, key: string): Promise<string | undefined> { async get(cacheName: string, key: string): Promise<string | undefined> {
await this._checkReady() await this._checkReady()
const cache = await this.redis?.hget(cacheName, key) const cache = await this.redis?.hget(cacheName, key)
if (cache === undefined) return if (cache === undefined) return
@ -84,10 +110,11 @@ export class RedisCacheAdapter implements ICacheAdapter {
} }
} }
async set ( async set(
cacheName: string, cacheName: string,
key: string, key: string,
value: any value: any,
expire?: number
): Promise<number | undefined> { ): Promise<number | undefined> {
await this._checkReady() await this._checkReady()
const result = await this.redis?.hset( const result = await this.redis?.hset(
@ -95,10 +122,17 @@ export class RedisCacheAdapter implements ICacheAdapter {
key, key,
typeof value === 'object' ? JSON.stringify(value) : value typeof value === 'object' ? JSON.stringify(value) : value
) )
if (expire !== undefined) {
await this.redis?.hset(
`${cacheName}:expires`,
key,
JSON.stringify({ name: cacheName, key, at: new Date().getTime() + expire })
)
}
return result return result
} }
async delete (cacheName: string, key: string): Promise<boolean> { async delete(cacheName: string, key: string): Promise<boolean> {
await this._checkReady() await this._checkReady()
const exists = await this.redis?.hexists(cacheName, key) const exists = await this.redis?.hexists(cacheName, key)
if (exists === 0) return false if (exists === 0) return false
@ -106,7 +140,7 @@ export class RedisCacheAdapter implements ICacheAdapter {
return true return true
} }
async array (cacheName: string): Promise<any[] | undefined> { async array(cacheName: string): Promise<any[] | undefined> {
await this._checkReady() await this._checkReady()
const data = await this.redis?.hvals(cacheName) const data = await this.redis?.hvals(cacheName)
return data?.map((e: string) => JSON.parse(e)) return data?.map((e: string) => JSON.parse(e))

View file

@ -7,7 +7,6 @@ import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UserManager } from '../managers/users.ts' import { UserManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts' import { GuildManager } from '../managers/guilds.ts'
import { ChannelsManager } from '../managers/channels.ts' import { ChannelsManager } from '../managers/channels.ts'
import { MessagesManager } from '../managers/messages.ts'
import { import {
ActivityGame, ActivityGame,
ClientActivity, ClientActivity,
@ -31,6 +30,8 @@ export interface ClientOptions {
bot?: boolean bot?: boolean
/** Force all requests to Canary API */ /** Force all requests to Canary API */
canary?: boolean canary?: boolean
/** Time till which Messages are to be cached, in MS. Default is 3600000 */
messageCacheLifetime?: number
} }
/** /**
@ -53,11 +54,12 @@ export class Client extends EventEmitter {
intents?: GatewayIntents[] intents?: GatewayIntents[]
/** Whether to force new session or not */ /** Whether to force new session or not */
forceNewSession?: boolean forceNewSession?: boolean
/** Time till messages to stay cached, in MS. */
messageCacheLifetime: number = 3600000
users: UserManager = new UserManager(this) users: UserManager = new UserManager(this)
guilds: GuildManager = new GuildManager(this) guilds: GuildManager = new GuildManager(this)
channels: ChannelsManager = new ChannelsManager(this) channels: ChannelsManager = new ChannelsManager(this)
messages: MessagesManager = new MessagesManager(this)
emojis: EmojisManager = new EmojisManager(this) emojis: EmojisManager = new EmojisManager(this)
/** Whether this client will login as bot user or not */ /** Whether this client will login as bot user or not */
@ -80,6 +82,7 @@ export class Client extends EventEmitter {
: new ClientPresence(options.presence) : new ClientPresence(options.presence)
if (options.bot === false) this.bot = false if (options.bot === false) this.bot = false
if (options.canary === true) this.canary = true if (options.canary === true) this.canary = true
if (options.messageCacheLifetime !== undefined) this.messageCacheLifetime = options.messageCacheLifetime
} }
/** Set Cache Adapter */ /** Set Cache Adapter */

View file

@ -4,6 +4,7 @@ import { TextChannel } from '../structures/textChannel.ts'
import { User } from '../structures/user.ts' import { User } from '../structures/user.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { CommandClient } from './commandClient.ts' import { CommandClient } from './commandClient.ts'
import { Extension } from "./extensions.ts"
export interface CommandContext { export interface CommandContext {
/** The Client object */ /** The Client object */
@ -35,8 +36,8 @@ export class Command {
description?: string description?: string
/** Array of Aliases of Command, or only string */ /** Array of Aliases of Command, or only string */
aliases?: string | string[] aliases?: string | string[]
/** Category of the Command */ /** Extension (Parent) of the Command */
category?: string extension?: Extension
/** Usage of Command, only Argument Names */ /** Usage of Command, only Argument Names */
usage?: string | string[] usage?: string | string[]
/** Usage Example of Command, only Arguments (without Prefix and Name) */ /** Usage Example of Command, only Arguments (without Prefix and Name) */
@ -45,6 +46,16 @@ export class Command {
args?: number | boolean args?: number | boolean
/** Permission(s) required for using Command */ /** Permission(s) required for using Command */
permissions?: string | string[] 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) */ /** Whether the Command can only be used in Guild (if allowed in DMs) */
guildOnly?: boolean guildOnly?: boolean
/** Whether the Command can only be used in Bot's DMs (if allowed) */ /** Whether the Command can only be used in Bot's DMs (if allowed) */
@ -58,13 +69,16 @@ export class Command {
execute(ctx: CommandContext): any { } execute(ctx: CommandContext): any { }
/** Method executed after executing command, passes on CommandContext and the value returned by execute too. (optional) */ /** Method executed after executing command, passes on CommandContext and the value returned by execute too. (optional) */
afterExecute(ctx: CommandContext, executeResult: any): any { } afterExecute(ctx: CommandContext, executeResult: any): any { }
toString(): string {
return `Command: ${this.name}${this.extension !== undefined ? ` [${this.extension.name}]` : ''}`
}
} }
export class CommandsManager { export class CommandsManager {
client: CommandClient client: CommandClient
list: Collection<string, Command> = new Collection() list: Collection<string, Command> = new Collection()
disabled: Set<string> = new Set() disabled: Set<string> = new Set()
disabledCategories: Set<string> = new Set()
constructor(client: CommandClient) { constructor(client: CommandClient) {
this.client = client this.client = client
@ -96,7 +110,6 @@ export class CommandsManager {
const cmd = this.find(name) const cmd = this.find(name)
if (cmd === undefined) return if (cmd === undefined) return
if (this.isDisabled(cmd) && bypassDisable !== true) return if (this.isDisabled(cmd) && bypassDisable !== true) return
if (cmd.category !== undefined && this.isCategoryDisabled(cmd.category) && bypassDisable !== true) return
return cmd return cmd
} }
@ -121,7 +134,7 @@ export class CommandsManager {
add(cmd: Command | typeof Command): boolean { add(cmd: Command | typeof Command): boolean {
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
if (!(cmd instanceof Command)) cmd = new cmd() if (!(cmd instanceof Command)) cmd = new cmd()
if (this.exists(cmd)) return false if (this.exists(cmd)) throw new Error(`Failed to add Command '${cmd.toString()}' with name/alias already exists.`)
this.list.set(cmd.name, cmd) this.list.set(cmd.name, cmd)
return true return true
} }
@ -133,11 +146,6 @@ export class CommandsManager {
else return this.list.delete(find.name) else return this.list.delete(find.name)
} }
/** Get all Commands of given Category */
category(name: string): Collection<string, Command> {
return this.list.filter(c => c.category === name)
}
/** Check whether a Command is disabled or not */ /** Check whether a Command is disabled or not */
isDisabled(name: string | Command): boolean { isDisabled(name: string | Command): boolean {
const cmd = typeof name === "string" ? this.find(name) : name const cmd = typeof name === "string" ? this.find(name) : name
@ -155,18 +163,6 @@ export class CommandsManager {
this.disabled.add(cmd.name) this.disabled.add(cmd.name)
return true return true
} }
/** Check whether a Category is disabled */
isCategoryDisabled(name: string): boolean {
return this.disabledCategories.has(name)
}
/** Disable a Category of Commands */
disableCategory(name: string): boolean {
if (this.isCategoryDisabled(name)) return false
this.disabledCategories.add(name)
return true
}
} }
export interface ParsedCommand { export interface ParsedCommand {

View file

@ -1,7 +1,8 @@
import { Embed, Message } from '../../mod.ts' import { Message } from "../../mod.ts"
import { awaitSync } from "../utils/mixedPromise.ts" import { awaitSync } from "../utils/mixedPromise.ts"
import { Client, ClientOptions } from './client.ts' import { Client, ClientOptions } from './client.ts'
import { CommandContext, CommandsManager, parseCommand } from './command.ts' import { CommandContext, CommandsManager, parseCommand } from './command.ts'
import { ExtensionsManager } from "./extensions.ts"
type PrefixReturnType = string | string[] | Promise<string | string[]> type PrefixReturnType = string | string[] | Promise<string | string[]>
@ -21,33 +22,6 @@ export interface CommandClientOptions extends ClientOptions {
caseSensitive?: boolean 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 { export class CommandClient extends Client implements CommandClientOptions {
prefix: string | string[] prefix: string | string[]
mentionPrefix: boolean mentionPrefix: boolean
@ -62,8 +36,8 @@ export class CommandClient extends Client implements CommandClientOptions {
allowBots: boolean allowBots: boolean
allowDMs: boolean allowDMs: boolean
caseSensitive: boolean caseSensitive: boolean
extensions: ExtensionsManager = new ExtensionsManager(this)
commands: CommandsManager = new CommandsManager(this) commands: CommandsManager = new CommandsManager(this)
texts: CommandTexts = DefaultCommandTexts
constructor(options: CommandClientOptions) { constructor(options: CommandClientOptions) {
super(options) super(options)
@ -159,31 +133,9 @@ export class CommandClient extends Client implements CommandClientOptions {
if (command === undefined) return if (command === undefined) return
const baseReplaces: Replaces = { if (command.whitelistedGuilds !== undefined && msg.guild !== undefined && command.whitelistedGuilds.includes(msg.guild.id) === false) return;
command: command.name, if (command.whitelistedChannels !== undefined && command.whitelistedChannels.includes(msg.channel.id) === false) return;
nameUsed: parsed.name, if (command.whitelistedUsers !== undefined && command.whitelistedUsers.includes(msg.author.id) === false) return;
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
}
const ctx: CommandContext = { const ctx: CommandContext = {
client: this, client: this,
@ -198,8 +150,41 @@ export class CommandClient extends Client implements CommandClientOptions {
guild: msg.guild guild: msg.guild
} }
if (command.ownerOnly === true && !this.owners.includes(msg.author.id)) return this.emit('commandOwnerOnly', ctx, command)
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.botPermissions !== undefined && msg.guild !== undefined) {
// TODO: Check Overwrites too
const me = await msg.guild.me()
const missing: string[] = []
for (const perm of command.botPermissions) {
if (me.permissions.has(perm) === false) missing.push(perm)
}
if (missing.length !== 0) return this.emit('commandBotMissingPermissions', ctx, command, missing)
}
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)
}
if (command.args !== undefined) {
if (typeof command.args === 'boolean' && parsed.args.length === 0) return this.emit('commandMissingArgs', ctx, command)
else if (typeof command.args === 'number' && parsed.args.length < command.args) this.emit('commandMissingArgs', ctx, command)
}
try { try {
this.emit('commandUsed', { context: ctx }) this.emit('commandUsed', ctx, command)
const beforeExecute = await awaitSync(command.beforeExecute(ctx)) const beforeExecute = await awaitSync(command.beforeExecute(ctx))
if (beforeExecute === false) return if (beforeExecute === false) return
@ -207,30 +192,7 @@ export class CommandClient extends Client implements CommandClientOptions {
const result = await awaitSync(command.execute(ctx)) const result = await awaitSync(command.execute(ctx))
command.afterExecute(ctx, result) command.afterExecute(ctx, result)
} catch (e) { } catch (e) {
if (this.texts.ERROR !== undefined) this.emit('commandError', command, ctx, e)
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)
} }
} }
} }

112
src/models/extensions.ts Normal file
View file

@ -0,0 +1,112 @@
import { Collection } from "../utils/collection.ts";
import { Command } from "./command.ts";
import { CommandClient } from "./commandClient.ts";
export type ExtensionEventCallback = (ext: Extension, ...args: any[]) => any
export class ExtensionCommands {
extension: Extension
constructor(ext: Extension) {
this.extension = ext
}
get list(): Collection<string, Command> {
return this.extension.client.commands.list.filter(c => c.extension?.name === this.extension.name)
}
get(cmd: string): Command | undefined {
const find = this.extension.client.commands.find(cmd)
// linter sucks
if (find === undefined) return undefined
else if (find.extension === undefined) return undefined
else if (find.extension.name !== this.extension.name) return undefined
else return find
}
add(Cmd: Command | typeof Command): boolean {
const cmd = Cmd instanceof Command ? Cmd : new Cmd()
cmd.extension = this.extension
return this.extension.client.commands.add(cmd)
}
delete(cmd: Command | string): boolean {
const find = this.extension.client.commands.find(typeof cmd === 'string' ? cmd : cmd.name)
if (find === undefined) return false
if (find.extension !== undefined && find.extension.name !== this.extension.name) return false
else return this.extension.client.commands.delete(find)
}
deleteAll(): void {
for (const [cmd] of this.list) {
this.delete(cmd)
}
}
}
export class Extension {
client: CommandClient
name: string = ''
description?: string
commands: ExtensionCommands = new ExtensionCommands(this)
events: { [name: string]: (...args: any[]) => {} } = {}
constructor(client: CommandClient) {
this.client = client
}
listen(event: string, cb: ExtensionEventCallback): boolean {
if (this.events[event] !== undefined) return false
else {
const fn = (...args: any[]): any => {
// eslint-disable-next-line standard/no-callback-literal
cb(this, ...args)
};
this.client.on(event, fn)
this.events[event] = fn
return true
}
}
load(): any {}
unload(): any {}
}
export class ExtensionsManager {
client: CommandClient
list: Collection<string, Extension> = new Collection()
constructor(client: CommandClient) {
this.client = client
}
get(ext: string): Extension | undefined {
return this.list.get(ext)
}
exists(ext: string): boolean {
return this.get(ext) !== undefined
}
load(ext: Extension | typeof Extension): void {
// eslint-disable-next-line new-cap
if (!(ext instanceof Extension)) ext = new ext(this.client)
if (this.exists(ext.name)) throw new Error(`Extension with name '${ext.name}' already exists`)
this.list.set(ext.name, ext)
ext.load()
}
unload(ext: Extension | string): boolean {
const name = typeof ext === 'string' ? ext : ext.name
const extension = this.get(name)
if (extension === undefined) return false
extension.commands.deleteAll()
for (const [k, v] of Object.entries(extension.events)) {
this.client.removeListener(k, v)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete extension.events[k]
}
extension.unload()
return this.list.delete(name)
}
}

View file

@ -8,6 +8,7 @@ import { GuildChannelsManager } from '../managers/guildChannels.ts'
import { MembersManager } from '../managers/members.ts' import { MembersManager } from '../managers/members.ts'
import { Role } from './role.ts' import { Role } from './role.ts'
import { GuildEmojisManager } from '../managers/guildEmojis.ts' import { GuildEmojisManager } from '../managers/guildEmojis.ts'
import { Member } from "./member.ts"
export class Guild extends Base { export class Guild extends Base {
id: string id: string
@ -214,4 +215,10 @@ export class Guild extends Base {
async getEveryoneRole (): Promise<Role> { async getEveryoneRole (): Promise<Role> {
return (await this.roles.array().then(arr => arr?.sort((b, a) => a.position - b.position)[0]) as any) as 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 { MemberRolesManager } from "../managers/memberRoles.ts"
import { Client } from '../models/client.ts' import { Client } from '../models/client.ts'
import { MemberPayload } from '../types/guild.ts' import { MemberPayload } from '../types/guild.ts'
import { Permissions } from "../utils/permissions.ts"
import { Base } from './base.ts' import { Base } from './base.ts'
import { Guild } from "./guild.ts" import { Guild } from "./guild.ts"
import { User } from './user.ts' import { User } from './user.ts'
@ -15,13 +16,12 @@ export class Member extends Base {
deaf: boolean deaf: boolean
mute: boolean mute: boolean
guild: Guild 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) super(client)
this.id = data.user.id this.id = data.user.id
this.user = user this.user = user
// this.user =
// cache.get('user', data.user.id) ?? new User(this.client, data.user)
this.nick = data.nick this.nick = data.nick
this.guild = guild this.guild = guild
this.roles = new MemberRolesManager(this.client, this.guild.roles, this) 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.premiumSince = data.premium_since
this.deaf = data.deaf this.deaf = data.deaf
this.mute = data.mute this.mute = data.mute
// TODO: Cache in Gateway Event Code if (perms !== undefined) this.permissions = perms
// cache.set('member', this.id, this) else this.permissions = new Permissions(Permissions.DEFAULT)
} }
protected readFromData (data: MemberPayload): void { protected readFromData (data: MemberPayload): void {

View file

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

View file

@ -1,3 +1,4 @@
import { MessagesManager } from "../../mod.ts"
import { Client } from '../models/client.ts' import { Client } from '../models/client.ts'
import { GuildTextChannelPayload, MessageOption, Overwrite, TextChannelPayload } from '../types/channel.ts' import { GuildTextChannelPayload, MessageOption, Overwrite, TextChannelPayload } from '../types/channel.ts'
import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts'
@ -11,9 +12,11 @@ type AllMessageOptions = MessageOption | Embed
export class TextChannel extends Channel { export class TextChannel extends Channel {
lastMessageID?: string lastMessageID?: string
lastPinTimestamp?: string lastPinTimestamp?: string
messages: MessagesManager
constructor (client: Client, data: TextChannelPayload) { constructor (client: Client, data: TextChannelPayload) {
super(client, data) super(client, data)
this.messages = new MessagesManager(this.client, this)
this.lastMessageID = data.last_message_id this.lastMessageID = data.last_message_id
this.lastPinTimestamp = data.last_pin_timestamp this.lastPinTimestamp = data.last_pin_timestamp
} }

View file

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

View file

@ -1,4 +1,11 @@
import { CommandClient, Intents } from '../../mod.ts' import { Command, CommandClient, Intents } from '../../mod.ts'
import { GuildChannel } from "../managers/guildChannels.ts"
import { CommandContext } from "../models/command.ts"
import { Extension } from "../models/extensions.ts"
import { Member } from "../structures/member.ts"
import { Message } from "../structures/message.ts"
import { Role } from "../structures/role.ts"
import { MessageDeletePayload } from "../types/gateway.ts"
import { TOKEN } from './config.ts' import { TOKEN } from './config.ts'
const client = new CommandClient({ const client = new CommandClient({
@ -13,10 +20,71 @@ client.on('ready', () => {
console.log(`[Login] Logged in as ${client.user?.tag}!`) console.log(`[Login] Logged in as ${client.user?.tag}!`)
}) })
client.on('messageDelete', (msg: Message) => {
console.log(`Message Deleted: ${msg.id}, ${msg.author.tag}, ${msg.content}`)
})
client.on('messageDeleteUncached', (d: MessageDeletePayload) => {
console.log(`Uncached Message Deleted: ${d.id} in ${d.channel_id}`)
})
client.on('messageUpdate', (before: Message, after: Message) => {
console.log('Message Update')
console.log(`Before: ${before.author.tag}: ${before.content}`)
console.log(`After: ${after.author.tag}: ${after.content}`)
})
client.on('messageUpdateUncached', (msg: Message) => {
console.log(`Message: ${msg.author.tag}: ${msg.content}`)
})
client.on('guildMemberAdd', (member: Member) => {
console.log(`Member Join: ${member.user.tag}`)
})
client.on('guildMemberRemove', (member: Member) => {
console.log(`Member Leave: ${member.user.tag}`)
})
client.on('guildRoleCreate', (role: Role) => {
console.log(`Role Create: ${role.name}`)
})
client.on('guildRoleDelete', (role: Role) => {
console.log(`Role Delete: ${role.name}`)
})
client.on('guildRoleUpdate', (role: Role, after: Role) => {
console.log(`Role Update: ${role.name}, ${after.name}`)
})
// client.on('messageCreate', msg => console.log(`${msg.author.tag}: ${msg.content}`)) // client.on('messageCreate', msg => console.log(`${msg.author.tag}: ${msg.content}`))
client.on("commandError", console.error) client.on("commandError", console.error)
class ChannelLog extends Extension {
onChannelCreate(ext: Extension, channel: GuildChannel): void {
console.log(`Channel Created: ${channel.name}`)
}
load(): void {
this.listen('channelCreate', this.onChannelCreate)
class Pong extends Command {
name = 'Pong'
execute(ctx: CommandContext): any {
ctx.message.reply('Ping!')
}
}
this.commands.add(Pong)
}
}
client.extensions.load(ChannelLog)
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async() => { ;(async() => {
const files = Deno.readDirSync('./src/test/cmds') const files = Deno.readDirSync('./src/test/cmds')

View file

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

View file

@ -186,7 +186,7 @@ export interface GuildBanRemovePayload {
export interface GuildEmojiUpdatePayload { export interface GuildEmojiUpdatePayload {
guild_id: string guild_id: string
emojis: [] emojis: EmojiPayload[]
} }
export interface GuildIntegrationsUpdatePayload { export interface GuildIntegrationsUpdatePayload {

View file

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

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: number
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(): number {
return this.bitfield
}
valueOf(): number {
return this.bitfield
}
*[Symbol.iterator](): Iterator<string> {
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
}
}

View file

@ -33,7 +33,7 @@ export const getBuildInfo = (
if (Deno.build.os === 'darwin') { if (Deno.build.os === 'darwin') {
os = 'MacOS' os = 'MacOS'
os_version = '10.15.6' os_version = '10.15.6'
browser = 'Safari' browser = 'Firefox'
browser_version = '14.0.1' browser_version = '14.0.1'
browser_user_agent = browser_user_agent =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15'

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)
}
}