diff --git a/README.md b/README.md index 1011bc643..86ecb644b 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,7 @@ The instructions below are meant for advanced developers! Starting with Discordeno is very simple, you can start from scratch without any boilerplates/frameworks: Add this snippet of code into a new TypeScript file: ```typescript -import StartBot from "https://x.nest.land/Discordeno@9.0.1/src/module/client.ts"; -import { sendMessage } from "https://x.nest.land/Discordeno@9.0.1/src/handlers/channel.ts"; -import { Intents } from "https://x.nest.land/Discordeno@9.0.1/src/types/options.ts"; +import StartBot, { sendMessage, Intents } from "https://x.nest.land/Discordeno@9.0.1/mod.ts"; import config from "./config.ts"; StartBot({ diff --git a/deps.ts b/deps.ts index fd14f318d..8e07e8ced 100644 --- a/deps.ts +++ b/deps.ts @@ -12,4 +12,4 @@ export { isWebSocketPongEvent, } from "https://deno.land/std@0.67.0/ws/mod.ts"; export type { WebSocket } from "https://deno.land/std@0.67.0/ws/mod.ts"; -export { inflate } from "https://deno.land/x/zlib.es@v1.0.0/mod.ts"; +export { decompress_with as inflate } from "https://unpkg.com/@evan/wasm@0.0.11/target/zlib/deno.js"; diff --git a/docs/content/advanced/dynamiccommands.md b/docs/content/advanced/dynamiccommands.md index 313ee0719..a68da76e1 100644 --- a/docs/content/advanced/dynamiccommands.md +++ b/docs/content/advanced/dynamiccommands.md @@ -54,43 +54,44 @@ const nekosEndpoints = [ { name: "gecg", path: "/img/gecg", nsfw: false }, { name: "avatar", path: "/img/avatar", nsfw: false }, { name: "waifu", path: "/img/waifu", nsfw: false }, - { name: "randomHentaiGif", path: "/img/Random_hentai_gif", nsfw: true }, - { name: "pussy", path: "/img/pussy", nsfw: true }, - { name: "nekoGif", path: "/img/nsfw_neko_gif", nsfw: true }, - { name: "neko", path: "/img/lewd", nsfw: true }, - { name: "lesbian", path: "/img/les", nsfw: true }, - { name: "kuni", path: "/img/kuni", nsfw: true }, - { name: "cumsluts", path: "/img/cum", nsfw: true }, - { name: "classic", path: "/img/classic", nsfw: true }, - { name: "boobs", path: "/img/boobs", nsfw: true }, - { name: "bJ", path: "/img/bj", nsfw: true }, - { name: "anal", path: "/img/anal", nsfw: true }, - { name: "avatar", path: "/img/nsfw_avatar", nsfw: true }, - { name: "yuri", path: "/img/yuri", nsfw: true }, - { name: "trap", path: "/img/trap", nsfw: true }, - { name: "tits", path: "/img/tits", nsfw: true }, - { name: "girlSoloGif", path: "/img/solog", nsfw: true }, - { name: "girlSolo", path: "/img/solo", nsfw: true }, - { name: "pussyWankGif", path: "/img/pwankg", nsfw: true }, - { name: "pussyArt", path: "/img/pussy_jpg", nsfw: true }, - { name: "kemonomimi", path: "/img/lewdkemo", nsfw: true }, - { name: "kitsune", path: "/img/lewdk", nsfw: true }, - { name: "keta", path: "/img/keta", nsfw: true }, - { name: "holo", path: "/img/hololewd", nsfw: true }, - { name: "holoEro", path: "/img/holoero", nsfw: true }, - { name: "hentai", path: "/img/hentai", nsfw: true }, - { name: "futanari", path: "/img/futanari", nsfw: true }, - { name: "femdom", path: "/img/femdom", nsfw: true }, - { name: "feetGif", path: "/img/feetg", nsfw: true }, - { name: "eroFeet", path: "/img/erofeet", nsfw: true }, - { name: "feet", path: "/img/feet", nsfw: true }, - { name: "ero", path: "/img/ero", nsfw: true }, - { name: "eroKitsune", path: "/img/erok", nsfw: true }, - { name: "eroKemonomimi", path: "/img/erokemo", nsfw: true }, - { name: "eroNeko", path: "/img/eron", nsfw: true }, - { name: "eroYuri", path: "/img/eroyuri", nsfw: true }, - { name: "cumArts", path: "/img/cum_jpg", nsfw: true }, - { name: "blowJob", path: "/img/blowjob", nsfw: true }, + // The follow name and paths have been hidden for this guide as they are NSFW. + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, ]; nekosEndpoints.forEach((endpoint) => { diff --git a/docs/content/gettingstarted.md b/docs/content/gettingstarted.md index 009065154..6ee051f24 100644 --- a/docs/content/gettingstarted.md +++ b/docs/content/gettingstarted.md @@ -73,8 +73,10 @@ Alternatively, you can use boilerplate template repositories that were created b | -------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Official Boilerplate | Skillz4Killz#4500 | [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template), [Support Server](https://discord.gg/J4NqJ72) | This is a very minimalistic design for a boilerplate for your bot to get you started. | | DenoBot | NTM Nathan#0001 | [GitHub](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | Another boilerplate example of the first one, with more commands and improvements. | +| Discordeno Helper | Suyashtnt | [Github](https://github.com/Suyashtnt/discordeno-helper-template/) | A reimplementation of DenoBot using the [discordeno-helper](https://github.com/Suyashtnt/discordeno-helper) framework + +**Open Sourced Bots:** -Open Sourced Bots: | Bot Name | Developer | Links | | ----------------- | ---------- | ---------------------------------------------------------- | | discordeno-mattis | Mattis6666 | [Github](https://github.com/Mattis6666/discordeno-mattis/) | diff --git a/docs/content/introduction.md b/docs/content/introduction.md index 29c6b408c..effb978f7 100644 --- a/docs/content/introduction.md +++ b/docs/content/introduction.md @@ -19,6 +19,12 @@ Discordeno is a Third Party Deno Library for interacting with the Discord API. - Latest and Greatest JavaScript - Actively Maintained! +### User Reviews + +If you wish to leave a review for other users, please send a PR adding your review to this section! + +Using the Discord API with types but such a simple language like TypeScript is so easy now. Discordeno is A W E S O M E! -[LukasDoesDev](https://github.com/LukasDoesDev) + ## Read me first... Discordeno is cool right? You could make the next big bot! Who knows, but before we get right into developing our Bot. We want to get started with learning the basics... diff --git a/egg.yml b/egg.yml index aaacd0120..408443064 100644 --- a/egg.yml +++ b/egg.yml @@ -2,9 +2,9 @@ name: Discordeno description: >- Discord Deno TypeScript API library wrapper(Officially vetted library by Discord Team) https://discordeno.netlify.app -version: 9.0.1 +version: 9.0.5 stable: true -entry: /mod.ts +entry: mod.ts repository: 'https://github.com/Skillz4Killz/Discordeno' files: - ./src/**/* @@ -13,7 +13,5 @@ files: - README.md - tsconfig.json - ./deps.ts - - ./mod.ts - - ./mod.ts checkAll: false unlisted: false diff --git a/src/controllers/members.ts b/src/controllers/members.ts index 6d413f90f..0083f158f 100644 --- a/src/controllers/members.ts +++ b/src/controllers/members.ts @@ -41,11 +41,6 @@ export async function handleInternalGuildMemberRemove(data: DiscordPayload) { member || payload.user, ); - eventHandlers.guildMemberRemove?.( - guild, - member || payload.user, - ); - guild.members.delete(payload.user.id); } diff --git a/src/controllers/roles.ts b/src/controllers/roles.ts index 9f889d23b..a1b4586d0 100644 --- a/src/controllers/roles.ts +++ b/src/controllers/roles.ts @@ -30,6 +30,11 @@ export async function handleInternalGuildRoleDelete(data: DiscordPayload) { const cachedRole = guild.roles.get(payload.role_id)!; guild.roles.delete(payload.role_id); eventHandlers.roleDelete?.(guild, cachedRole); + + // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. + guild.members.forEach(member => { + member.roles = member.roles.filter(id => id !== payload.role_id); + }); } export async function handleInternalGuildRoleUpdate(data: DiscordPayload) { diff --git a/src/handlers/guild.ts b/src/handlers/guild.ts index 741f7b2f0..b045affe6 100644 --- a/src/handlers/guild.ts +++ b/src/handlers/guild.ts @@ -7,33 +7,23 @@ import type { Guild } from "../structures/guild.ts"; import type { Member } from "../structures/member.ts"; import { structures } from "../structures/mod.ts"; import type { ImageFormats, ImageSize } from "../types/cdn.ts"; -import { - ChannelCreatePayload, - ChannelTypes, -} from "../types/channel.ts"; +import { ChannelCreatePayload, ChannelTypes } from "../types/channel.ts"; import { Errors } from "../types/errors.ts"; import type { BannedUser, BanOptions, - ChannelCreateOptions, CreateEmojisOptions, - CreateRoleOptions, - CreateServerOptions, EditEmojisOptions, - EditIntegrationOptions, FetchMembersOptions, GetAuditLogsOptions, - GuildEditOptions, PositionSwap, - PruneOptions, PrunePayload, - UserPayload, } from "../types/guild.ts"; import type { MemberCreatePayload } from "../types/member.ts"; diff --git a/src/module/basicShard.ts b/src/module/basicShard.ts index 6d92e3115..60717ca3c 100644 --- a/src/module/basicShard.ts +++ b/src/module/basicShard.ts @@ -14,17 +14,16 @@ import type { } from "../types/discord.ts"; import { GatewayOpcode } from "../types/discord.ts"; import type { FetchMembersOptions } from "../types/guild.ts"; -import { Collection } from "../utils/collection.ts"; import type { BotStatusRequest } from "../utils/utils.ts"; import type { IdentifyPayload } from "./client.ts"; -import { - botGatewayData, - eventHandlers, -} from "./client.ts"; +import { botGatewayData, eventHandlers } from "./client.ts"; import { handleDiscordPayload } from "./shardingManager.ts"; -export const basicShards = new Collection(); -const heartbeating = new Set(); +const basicShards = new Map(); +const heartbeating = new Map(); +const utf8decoder = new TextDecoder(); +const RequestMembersQueue: RequestMemberQueuedRequest[] = []; +let processQueue = false; export interface BasicShard { id: number; @@ -35,9 +34,6 @@ export interface BasicShard { needToResume: boolean; } -const RequestMembersQueue: RequestMemberQueuedRequest[] = []; -let processQueue = false; - interface RequestMemberQueuedRequest { guildID: string; shardID: number; @@ -110,7 +106,11 @@ export async function createBasicShard( } if (message instanceof Uint8Array) { - message = new TextDecoder().decode(inflate(message as Uint8Array)); + message = inflate( + message, + 0, + (slice: Uint8Array) => utf8decoder.decode(slice), + ); } if (typeof message === "string") { @@ -122,9 +122,13 @@ export async function createBasicShard( heartbeat( basicShard, (data.d as DiscordHeartbeatPayload).heartbeat_interval, + identifyPayload, ); } break; + case GatewayOpcode.HeartbeatACK: + heartbeating.set(shardID, true); + break; case GatewayOpcode.Reconnect: eventHandlers.debug?.( { type: "reconnect", data: { shardID: basicShard.id } }, @@ -199,17 +203,39 @@ function resume(shard: BasicShard, payload: IdentifyPayload) { })); } -// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume. async function heartbeat( shard: BasicShard, interval: number, + payload: IdentifyPayload, ) { + // We lost socket connection between heartbeats, resume connection if (shard.socket.isClosed) { + shard.needToResume = true; + resumeConnection(botGatewayData, payload, shard.id); heartbeating.delete(shard.id); return; } - if (!heartbeating.has(shard.id)) heartbeating.add(shard.id); + if (heartbeating.has(shard.id)) { + const receivedACK = heartbeating.get(shard.id); + // If a ACK response was not received since last heartbeat, issue invalid session close + if (!receivedACK) { + eventHandlers.debug?.( + { + type: "heartbeatStopped", + data: { + interval, + previousSequenceNumber: shard.previousSequenceNumber, + shardID: shard.id, + }, + }, + ); + return shard.socket.send(JSON.stringify({ op: 4009 })); + } + } + + // Set it to false as we are issuing a new heartbeat + heartbeating.set(shard.id, false); shard.socket.send( JSON.stringify( @@ -227,7 +253,7 @@ async function heartbeat( }, ); await delay(interval); - heartbeat(shard, interval); + heartbeat(shard, interval, payload); } async function resumeConnection( diff --git a/src/module/shardingManager.ts b/src/module/shardingManager.ts index 48c3ffb08..4b1cb2d31 100644 --- a/src/module/shardingManager.ts +++ b/src/module/shardingManager.ts @@ -15,12 +15,7 @@ import { requestGuildMembers, } from "./basicShard.ts"; import type { IdentifyPayload } from "./client.ts"; -import { - botGatewayData, - eventHandlers, - - identifyPayload, -} from "./client.ts"; +import { botGatewayData, eventHandlers, identifyPayload } from "./client.ts"; let shardCounter = 0; let basicSharding = false; diff --git a/src/structures/channel.ts b/src/structures/channel.ts index 64e77ef9b..00e28eaff 100644 --- a/src/structures/channel.ts +++ b/src/structures/channel.ts @@ -20,7 +20,7 @@ export async function createChannel( const channel = { ...rest, /** The guild id of the channel if it is a guild channel. */ - guildID: guildID || rawGuildID, + guildID: guildID || rawGuildID || "", /** The id of the last message sent in this channel */ lastMessageID, /** The amount of users allowed in this voice channel. */ diff --git a/src/types/activity.ts b/src/types/activity.ts index 81a180b4b..0299625ff 100644 --- a/src/types/activity.ts +++ b/src/types/activity.ts @@ -18,4 +18,6 @@ export enum ActivityType { Listening, /** Example: ":smiley: I am cool" */ Custom = 4, + /** Example: "Competing in Arena World Champions" */ + Competing, } diff --git a/src/types/options.ts b/src/types/options.ts index 1ad67dc6c..641432ada 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -6,10 +6,8 @@ import type { Role } from "../structures/role.ts"; import type { DiscordPayload, Emoji, - PresenceUpdatePayload, Properties, - TypingStartPayload, VoiceStateUpdatePayload, } from "./discord.ts"; @@ -17,10 +15,8 @@ import type { UserPayload } from "./guild.ts"; import type { Attachment, BaseMessageReactionPayload, - Embed, MessageReactionRemoveEmojiPayload, - MessageReactionUncachedPayload, PartialMessage, ReactionPayload, @@ -68,6 +64,7 @@ export interface DebugArg { | "requestManagerFetched" | "requestMembersProcessing" | "heartbeat" + | "heartbeatStopped" | "createShard" | "invalidSession" | "reconnect" diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 37e9fac90..d904a401c 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -58,6 +58,8 @@ export async function botHasPermission( const permissionBits = member.roles .map((id) => guild.roles.get(id)!) + // Remove any edge case undefined + .filter((r) => r) .reduce((bits, data) => { bits |= BigInt(data.permissions);