From ba68ad33bed51faab874d949fdcb34d2f575e9e8 Mon Sep 17 00:00:00 2001 From: pakkographic Date: Mon, 2 Sep 2024 21:54:54 +0200 Subject: [PATCH] Data & Analytics --- README.md | 9 ++++++ lib/rest/RequestHandler.ts | 5 +++ lib/structures/Client.ts | 28 +++++++++++++++-- lib/structures/TextChannel.ts | 2 +- lib/types/client.d.ts | 22 +++++++++++++ lib/types/misc.d.ts | 10 +++++- lib/util/Util.ts | 59 +++++++++++++++++++++++++++++++++-- 7 files changed, 129 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c0c8d6a..7f7efad 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,15 @@ npm install touchguild@dev The documentation under `dev` is always for the latest commit. If something isn't working that's in the documentation, you're likely looking at the wrong documentation. +## 🔬 Data & Analytics +Data collection is enabled by default on development builds for improving and making stats. + +This includes collecting application IDs, build info, method execution counts, and application command usage. + +For transparency, you can review the source code. + +If this is a concern, consider disabling the `dataCollecting` client option. +
## Links: diff --git a/lib/rest/RequestHandler.ts b/lib/rest/RequestHandler.ts index 65549cf..ebee26a 100644 --- a/lib/rest/RequestHandler.ts +++ b/lib/rest/RequestHandler.ts @@ -97,6 +97,11 @@ export class RequestHandler { options.method = options.method.toUpperCase() as RESTMethod; + // Data collection if enabled, counting the amount of request (all, authenticated, unauthenticated). + if (options.auth) void this.#manager.client.util.requestDataCollection({ event: "auth_request" }); + if (!options.auth && options.path !== "/api/science") + void this.#manager.client.util.requestDataCollection({ event: "unauth_request" }); + let reqBody: string | FormData | undefined; if (options.method !== "GET") { let stringBody: string | undefined; diff --git a/lib/structures/Client.ts b/lib/structures/Client.ts index 7510e7a..20f3fa2 100644 --- a/lib/structures/Client.ts +++ b/lib/structures/Client.ts @@ -85,6 +85,7 @@ export class Client extends TypedEmitter { isOfficialMarkdownEnabled: params.isOfficialMarkdownEnabled ?? true, wsReconnect: params.wsReconnect, collectionLimits: { + interactions: params.collectionLimits?.interactions ?? 100, messages: params.collectionLimits?.messages ?? 100, threads: params.collectionLimits?.threads ?? 100, threadComments: params.collectionLimits?.threadComments ?? 100, @@ -98,7 +99,8 @@ export class Client extends TypedEmitter { }, applicationShortname: params.applicationShortname, restMode: false, - intents: params.intents ?? [] + intents: params.intents ?? [], + dataCollection: params.dataCollection }; this.ws = new WSManager(this, { token: this.token, client: this, reconnect: params.wsReconnect }); this.guilds = new TypedCollection(Guild, this); @@ -126,10 +128,14 @@ export class Client extends TypedEmitter { "Application shortname is invalid, " + "requirements: \"1-32 characters containing no capital letters, spaces, or symbols other than - and _\"." ); + + if (this.application.enabled) + void this.util.requestDataCollection({ event: "application_command_enabled" }); } private async checkForUpdate(): Promise { this.lastCheckForUpdate = Date.now(); + void this.util.requestDataCollection({ event: "check_for_update" }); interface jsonRes { version: string; @@ -204,6 +210,7 @@ export class Client extends TypedEmitter { } } + void this.util.requestDataCollection({ event: "register_application_command" }); this.application.commands.push(command); } @@ -274,6 +281,7 @@ export class Client extends TypedEmitter { }); this.startTime = Date.now(); this.emit("ready"); + void this.util.requestDataCollection({ event: "gateway_connection" }); }); this.ws.on("disconnect", err => { @@ -288,9 +296,13 @@ export class Client extends TypedEmitter { disconnect(crashOnDisconnect?: boolean): void { if (this.ws.alive === false) return console.warn("There is no open connection."); + void this.util.requestDataCollection({ event: "request_disconnect" }); this.ws.disconnect(false); // closing all connections. console.log("The connection has been terminated."); - if (crashOnDisconnect) throw new Error("Connection closed."); + if (crashOnDisconnect) { + void this.util.requestDataCollection({ event: "crash_disconnect" }); + throw new Error("Connection closed."); + } } /** This method is used to get a specific guild channel, if cached. @@ -305,6 +317,7 @@ export class Client extends TypedEmitter { getChannel(guildID: string, channelID: string): T | undefined { if (!guildID) throw new Error("guildID is a required parameter."); if (!channelID) throw new Error("channelID is a required parameter."); + void this.util.requestDataCollection({ event: "cache_get_channel" }); return this.guilds.get(guildID)?.channels.get(channelID) as T; } @@ -317,6 +330,7 @@ export class Client extends TypedEmitter { */ getGuild(guildID: string): Guild | undefined { if (!guildID) throw new Error("guildID is a required parameter."); + void this.util.requestDataCollection({ event: "cache_get_guild" }); return this.guilds.get(guildID); } @@ -332,6 +346,7 @@ export class Client extends TypedEmitter { getMember(guildID: string, memberID: string): Member | undefined { if (!guildID) throw new Error("guildID is a required parameter."); if (!memberID) throw new Error("memberID is a required parameter."); + void this.util.requestDataCollection({ event: "cache_get_member" }); return this.getGuild(guildID)?.members.get(memberID); } @@ -345,6 +360,7 @@ export class Client extends TypedEmitter { */ getMembers(guildID: string): Array | undefined { if (!guildID) throw new Error("guildID is a required parameter."); + void this.util.requestDataCollection({ event: "cache_get_members" }); return this.getGuild(guildID)?.members.map(member => member); } @@ -360,7 +376,9 @@ export class Client extends TypedEmitter { */ getMessage(guildID: string, channelID: string, messageID: string): Message | undefined { const channel = this.getChannel(guildID, channelID); + void this.util.requestDataCollection({ event: "request_cache_get_message" }); if (channel instanceof TextChannel) { + void this.util.requestDataCollection({ event: "cache_get_message" }); return channel?.messages.get(messageID) as Message; } } @@ -374,7 +392,9 @@ export class Client extends TypedEmitter { */ getMessages(guildID: string, channelID: string): Array> | undefined { const channel = this.getChannel(guildID, channelID); + void this.util.requestDataCollection({ event: "request_cache_get_messages" }); if (channel instanceof TextChannel) { + void this.util.requestDataCollection({ event: "cache_get_messages" }); return channel?.messages.map(msg => msg); } } @@ -385,6 +405,7 @@ export class Client extends TypedEmitter { * @param command Application Command. */ registerGlobalApplicationCommand(command: ApplicationCommand): void { + void this.util.requestDataCollection({ event: "register_global_application_command" }); return this.registerApplicationCommand(command); } @@ -395,6 +416,7 @@ export class Client extends TypedEmitter { * @param command Application Command. */ registerGuildApplicationCommand(guildID: string, command: ApplicationCommand): void { + void this.util.requestDataCollection({ event: "register_guild_application_command" }); return this.registerApplicationCommand({ private: true, guildID, ...command }); } @@ -405,6 +427,7 @@ export class Client extends TypedEmitter { * @param command Application Command. */ registerUserApplicationCommand(userID: string, command: ApplicationCommand): void { + void this.util.requestDataCollection({ event: "register_user_application_command" }); return this.registerApplicationCommand({ private: true, userID, ...command }); } @@ -416,6 +439,7 @@ export class Client extends TypedEmitter { if (this.shouldCheckForUpdate) void this.checkForUpdate(); this.params.restMode = true; if (this.params.connectionMessage) console.log("> REST Mode has been enabled."); + void this.util.requestDataCollection({ event: "enable_rest_mode" }); void this.rest.misc.getAppUser().then(async () => { await this.rest.misc.getUserGuilds("@me").catch(() => []) .then(guilds => { diff --git a/lib/structures/TextChannel.ts b/lib/structures/TextChannel.ts index 817b110..9a897ba 100644 --- a/lib/structures/TextChannel.ts +++ b/lib/structures/TextChannel.ts @@ -45,7 +45,7 @@ export class TextChannel extends GuildChannel { this.interactions = new TypedCollection( CommandInteraction, client, - client.params.collectionLimits?.messages + client.params.collectionLimits?.interactions ); this.messages = new TypedCollection( Message, diff --git a/lib/types/client.d.ts b/lib/types/client.d.ts index f2cdfa6..4a2f121 100644 --- a/lib/types/client.d.ts +++ b/lib/types/client.d.ts @@ -24,6 +24,7 @@ export interface ClientOptions { calendarComments?: number; docComments?: number; docs?: number; + interactions?: number; messages?: number; scheduledEvents?: number; scheduledEventsRSVPS?: number; @@ -35,6 +36,26 @@ export interface ClientOptions { * connection is successfully established. */ connectionMessage?: boolean; + /** + * Consent to data collection, enabling us to improve the library, + * make statistics out of the data we collect, potentially leading us to make decisions based on them. + * + * It can also be used to promote the TouchGuild library and deliver specific information like the + * average time in ms the gateway takes, leading us to optimize latency, and deliver a better & faster library. + * + * We're using an API to forward collected data. + * + * **What is collected?** + * - IDs of the application, including the owner of it, app shortname. + * - The build you're using, stable or dev. + * - Amount of execution per method. + * - Usage of Application Commands (boolean) + * + * Data collecting is enabled by default if you use the development build. + * + * Transparency is key, open-source is transparent, feel free to check the source-code. + */ + dataCollection?: boolean; /** * **NOT RECOMMENDED, CAN BREAK THINGS** * @@ -87,6 +108,7 @@ export interface ClientOptions { * @default true */ wsReconnect?: boolean; + } export interface RESTOptions { diff --git a/lib/types/misc.d.ts b/lib/types/misc.d.ts index d3ca73e..df2be85 100644 --- a/lib/types/misc.d.ts +++ b/lib/types/misc.d.ts @@ -5,4 +5,12 @@ // Copyright (c) 2024 DinographicPixels. All rights reserved. // -export {}; +export interface DataCollectionProfile { + appID: string; + appName: string; + appShortname: string; + appUserID: string; + build: "stable" | "dev"; + buildVersion: string; + ownerID: string; +} diff --git a/lib/util/Util.ts b/lib/util/Util.ts index a800734..baa06aa 100644 --- a/lib/util/Util.ts +++ b/lib/util/Util.ts @@ -38,6 +38,8 @@ import { Subscription } from "../structures/Subscription"; import { Category } from "../structures/Category"; import { Message } from "../structures/Message"; import { GatewayLayerIntent, InteractionComponentType } from "../Constants"; +import type { DataCollectionProfile } from "../types/misc"; +import { config } from "../../pkgconfig"; import type { APIURLSignature } from "guildedapi-types.ts/v1"; import { fetch } from "undici"; @@ -47,13 +49,39 @@ export class Util { this.#client = client; } + /** This is the Data Collection Profile, sent if dataCollection is enabled. */ + private async getDataCollectionProfile(): Promise> { + await this.waitForObject(this.#client.user); + return { + appID: this.#client.user?.appID, + appName: this.#client.user?.username, + appShortname: this.#client.application.appShortname, + appUserID: this.#client.user?.id, + build: config.branch.toLowerCase().includes("development") ? "dev" : "stable", + buildVersion: config.version, + ownerID: this.#client.user?.ownerID + }; + } + + private waitForObject(object?: object, ms = 5000): Promise { + return new Promise(resolve => { + const checkUser = (): void => { + if (object) { + resolve(); + } else { + setTimeout(checkUser, ms); + } + }; + checkUser(); + }); + } + async bulkAddComponents( channelID: string, components: Array, message: Message, pushComponents = true ): Promise> { - // TODO: Enhance errors/ add more of them making them easier to understand. for (const component of components) { if (component.type === InteractionComponentType.BUTTON) { const regExpCheck = /^[\w-]{1,32}$/; @@ -75,7 +103,12 @@ export class Util { "emote availability or any other issue that could cause this error." ); }); - if (pushComponents) message.components.push(component); + if (pushComponents) { + message.components.push(component); + void this.#client.util.requestDataCollection({ event: "button_component_add" }); + } else { + void this.#client.util.requestDataCollection({ event: "button_component_update" }); + } } } return message; @@ -196,6 +229,28 @@ export class Util { return this.#client.params.intents?.includes(GatewayLayerIntent.ALL) || intents.some(intent => this.#client.params.intents?.includes(intent) ?? false); } + async requestDataCollection( + collect: { + data?: { + message: string; + }; + event: string; + } + ): Promise { + if (this.#client.params.dataCollection === false) return; + if (this.#client.params.dataCollection === undefined && (await this.getDataCollectionProfile()).build !== "dev") return; + + return void this.#client.rest.request({ + auth: false, + method: "POST", + route: "https://dinographicpixels.com/", + path: "api/science", + json: { + profile: await this.getDataCollectionProfile(), + collect + } + }).catch(err => this.#client.emit("error", err as Error)); + } updateChannel(data: RawChannel): T { if (data.serverId) { const guild = this.#client.guilds.get(data.serverId);