From 26a58c2119951e47cdcc42fc05643f24b3adab26 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 26 May 2025 00:29:12 +0700 Subject: [PATCH 01/48] fix: fix GET /bots/votes not working --- src/structs/Api.ts | 9 +++++---- src/typings.ts | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 74aaf73..0dc25e6 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -46,10 +46,11 @@ export class Api extends EventEmitter { throw new Error("Got a malformed API token."); } - const tokenData = atob(tokenSegments[1]); - try { - JSON.parse(tokenData).id; + const tokenData = atob(tokenSegments[1]); + const tokenId = JSON.parse(tokenData).id; + + options.id ??= tokenId; } catch { throw new Error( "Invalid API token state, this should not happen! Please report!" @@ -286,7 +287,7 @@ export class Api extends EventEmitter { * @returns {ShortUser[]} Array of unique users who've voted */ public async getVotes(page?: number): Promise { - return this._request("GET", "/bots/votes", { page: page ?? 1 }); + return this._request("GET", `/bots/${this.options.id}/votes`, { page: page ?? 1 }); } /** diff --git a/src/typings.ts b/src/typings.ts index f891c54..c7e04b3 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -1,6 +1,9 @@ export interface APIOptions { /** Top.gg token */ token?: string; + + /** Discord bot ID */ + id?: string; } /** Discord ID */ From d2cb2ce7765cfa6052c596f131b803ecbe432b30 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 17 Jun 2025 16:29:31 +0700 Subject: [PATCH 02/48] feat: add widgets --- src/index.ts | 1 + src/structs/Api.ts | 14 ++------------ src/structs/Widget.ts | 18 ++++++++++++++++++ src/typings.ts | 6 +++++- tests/Api.test.ts | 2 +- tests/jest.setup.ts | 2 +- tests/mocks/endpoints.ts | 9 +++++++-- 7 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 src/structs/Widget.ts diff --git a/src/index.ts b/src/index.ts index 1d7c6b4..f32d5ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from "./structs/Api"; export * from "./structs/Webhook"; +export * from "./structs/Widget"; export * from "./typings"; diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 0dc25e6..de51ebc 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -72,7 +72,7 @@ export class Api extends EventEmitter { if (this.options.token) headers["authorization"] = this.options.token; if (method !== "GET") headers["content-type"] = "application/json"; - let url = `https://top.gg/api${path}`; + let url = `https://top.gg/api/v1${path}`; if (body && method === "GET") url += `?${new URLSearchParams(body)}`; @@ -204,12 +204,7 @@ export class Api extends EventEmitter { * * @example * ```js - * // Finding by properties - * await api.getBots({ - * search: { - * username: "shiro" - * }, - * }); + * await api.getBots(); * // => * { * results: [ @@ -252,11 +247,6 @@ export class Api extends EventEmitter { public async getBots(query?: BotsQuery): Promise { if (query) { if (Array.isArray(query.fields)) query.fields = query.fields.join(", "); - if (query.search instanceof Object) { - query.search = Object.entries(query.search) - .map(([key, value]) => `${key}: ${value}`) - .join(" "); - } } return this._request("GET", "/bots", query); } diff --git a/src/structs/Widget.ts b/src/structs/Widget.ts new file mode 100644 index 0000000..daabe85 --- /dev/null +++ b/src/structs/Widget.ts @@ -0,0 +1,18 @@ +import { Snowflake } from "../typings"; + +const BASE_URL: string = "https://top.gg/api/v1"; + +/** + * Widget generator functions. + */ +export class Widget { + /** + * Generates a large widget URL. + * + * @param {Snowflake} id The ID. + * @returns {string} The widget URL. + */ + public static large(id: Snowflake): string { + return `${BASE_URL}/widgets/large/${id}`; + } +} diff --git a/src/typings.ts b/src/typings.ts index c7e04b3..5ca8965 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -171,7 +171,11 @@ export interface BotsQuery { limit?: number; /** Amount of bots to skip */ offset?: number; - /** A search string in the format of "field: value field2: value2" */ + /** + * A search string in the format of "field: value field2: value2" + * + * @deprecated No longer supported by Top.gg API v1. + */ search?: | { [key in keyof BotInfo]: string; diff --git a/tests/Api.test.ts b/tests/Api.test.ts index f163b42..0d6dda3 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -23,7 +23,7 @@ describe('API postStats test', () => { describe('API getStats test', () => { it('getStats should return 200 when bot is found', async () => { - expect(client.getStats('1')).resolves.toStrictEqual({ + expect(client.getStats()).resolves.toStrictEqual({ serverCount: BOT_STATS.server_count, shardCount: BOT_STATS.shard_count, shards: BOT_STATS.shards diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index 7d4db81..f8af465 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -10,7 +10,7 @@ interface IOptions { export const getIdInPath = (pattern: string, url: string) => { const regex = new RegExp(`^${pattern.replace(/:[^/]+/g, '([^/]+)')}$`); - const match = url.match(regex); + const match = url.split('?')[0].match(regex); return match ? match[1] : null; }; diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index ce1fca9..868fc18 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -21,10 +21,15 @@ export const endpoints = [ } }, { - pattern: '/api/bots/votes', + pattern: '/api/bots/:bot_id/votes', method: 'GET', data: VOTES, - requireAuth: true + requireAuth: true, + validate: (request: MockInterceptor.MockResponseCallbackOptions) => { + const bot_id = getIdInPath('/api/bots/:bot_id/votes', request.path); + if (Number(bot_id) === 0) return { statusCode: 404 }; + return null; + } }, { pattern: '/api/bots/check', From 2327b15f7863d76fcec0b6738e41ac75cb2efb73 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 18 Jun 2025 21:00:24 +0700 Subject: [PATCH 03/48] feat: add small widgets --- src/structs/Widget.ts | 46 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/structs/Widget.ts b/src/structs/Widget.ts index daabe85..084d77a 100644 --- a/src/structs/Widget.ts +++ b/src/structs/Widget.ts @@ -2,6 +2,14 @@ import { Snowflake } from "../typings"; const BASE_URL: string = "https://top.gg/api/v1"; +/** + * Widget type. + */ +export enum WidgetType { + DiscordBot = "discord/bot", + DiscordServer = "discord/server" +} + /** * Widget generator functions. */ @@ -9,10 +17,44 @@ export class Widget { /** * Generates a large widget URL. * + * @param {WidgetType} ty The widget type. + * @param {Snowflake} id The ID. + * @returns {string} The widget URL. + */ + public static large(ty: WidgetType, id: Snowflake): string { + return `${BASE_URL}/widgets/large/${ty}/${id}`; + } + + /** + * Generates a small widget URL for displaying votes. + * + * @param {WidgetType} ty The widget type. + * @param {Snowflake} id The ID. + * @returns {string} The widget URL. + */ + public static votes(ty: WidgetType, id: Snowflake): string { + return `${BASE_URL}/widgets/small/votes/${ty}/${id}`; + } + + /** + * Generates a small widget URL for displaying an entity's owner. + * + * @param {WidgetType} ty The widget type. + * @param {Snowflake} id The ID. + * @returns {string} The widget URL. + */ + public static owner(ty: WidgetType, id: Snowflake): string { + return `${BASE_URL}/widgets/small/owner/${ty}/${id}`; + } + + /** + * Generates a small widget URL for displaying social stats. + * + * @param {WidgetType} ty The widget type. * @param {Snowflake} id The ID. * @returns {string} The widget URL. */ - public static large(id: Snowflake): string { - return `${BASE_URL}/widgets/large/${id}`; + public static social(ty: WidgetType, id: Snowflake): string { + return `${BASE_URL}/widgets/small/social/${ty}/${id}`; } } From 13982a58e0618ff14a6d737cb1809de1eac60a0c Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:51:11 +0700 Subject: [PATCH 04/48] style: remove trailing comma --- src/structs/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index de51ebc..022bb88 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -111,7 +111,7 @@ export class Api extends EventEmitter { * @example * ```js * await api.postStats({ - * serverCount: 28199, + * serverCount: 28199 * }); * ``` * From 46ef8d168174208c294b4597406c8ded09267cb1 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 23 Jun 2025 23:34:01 +0700 Subject: [PATCH 05/48] docs: readme overhaul --- README.md | 161 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 129 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 156cae7..d91dc94 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,159 @@ -# Top.gg Node SDK +# Top.gg Node.js SDK -An official module for interacting with the Top.gg API +The community-maintained Node.js library for Top.gg. -# Installation +## Installation -`yarn add @top-gg/sdk` or `npm i @top-gg/sdk` +### NPM -# Introduction +```sh +$ npm i @top-gg/sdk +``` + +### Yarn + +```sh +$ yarn add @top-gg/sdk +``` + +## Setting up + +### CommonJS + +```js +const Topgg = require("@top-gg/sdk"); + +const client = new Topgg.Api(process.env.TOPGG_TOKEN); +``` + +### ES module + +```js +import Topgg from "@top-gg/sdk"; + +const client = new Topgg.Api(process.env.TOPGG_TOKEN); +``` + +## Usage + +### Getting a bot + +```js +const bot = await client.getBot("461521980492087297"); +``` + +### Getting several bots + +```js +const bots = await client.getBots(); +``` + +### Getting your bot's voters + +#### First page + +```js +const voters = await client.getVotes(); +``` + +#### Subsequent pages + +```js +const voters = await client.getVotes(2); +``` -The base client is Topgg.Api, and it takes your Top.gg token and provides you with plenty of methods to interact with the API. +### Check if a user has voted for your bot -See [this tutorial](https://github.com/top-gg/rust-sdk/assets/60427892/d2df5bd3-bc48-464c-b878-a04121727bff) on how to retrieve your API token. +```js +const hasVoted = await client.hasVoted("205680187394752512"); +``` -You can also setup webhooks via Topgg.Webhook, look down below at the examples for how to do so! +### Getting your bot's server count -# Links +```js +const { serverCount } = await client.getStats(); +``` -[Documentation](https://topgg.js.org) +### Posting your bot's server count -[API Reference](https://docs.top.gg) | [GitHub](https://github.com/top-gg/node-sdk) | [NPM](https://npmjs.com/package/@top-gg/sdk) | [Discord Server](https://discord.gg/EYHTgJX) +```js +await client.postStats({ + serverCount: bot.getServerCount() +}); +``` -# Popular Examples +### Automatically posting your bot's server count every few minutes -## Auto-Posting stats +You would need to use the third-party `topgg-autoposter` package to be able to autopost. Install it in your terminal like so: -If you're looking for an easy way to post your bot's stats (server count, shard count), check out [`topgg-autoposter`](https://npmjs.com/package/topgg-autoposter) +#### NPM + +```sh +$ npm i topgg-autoposter +``` + +#### Yarn + +```sh +$ yarn add topgg-autoposter +``` + +Then in your code: + +#### CommonJS ```js -const client = Discord.Client(); // Your discord.js client or any other const { AutoPoster } = require("topgg-autoposter"); -AutoPoster("topgg-token", client).on("posted", () => { +// Your discord.js client or any other +const client = Discord.Client(); + +AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { + console.log("Posted stats to Top.gg!"); +}); +``` + +#### ES module + +```js +import { AutoPoster } from "topgg-autoposter"; + +// Your discord.js client or any other +const client = Discord.Client(); + +AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { console.log("Posted stats to Top.gg!"); }); ``` -With this your server count and shard count will be posted to Top.gg +### Checking if the weekend vote multiplier is active + +```js +const isWeekend = await client.isWeekend(); +``` -## Webhook server +### Generating widget URLs + +#### Large ```js -const express = require("express"); -const Topgg = require("@top-gg/sdk"); +const widgetUrl = Topgg.Widget.large(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` -const app = express(); // Your express app +#### Votes -const webhook = new Topgg.Webhook("topggauth123"); // add your Top.gg webhook authorization (not bot token) +```js +const widgetUrl = Topgg.Widget.votes(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` -app.post( - "/dblwebhook", - webhook.listener((vote) => { - // vote is your vote object - console.log(vote.user); // 221221226561929217 - }) -); // attach the middleware +#### Owner -app.listen(3000); // your port +```js +const widgetUrl = Topgg.Widget.owner(Topgg.WidgetType.DiscordBot, "574652751745777665"); ``` -With this example, your webhook dashboard (`https://top.gg/bot/{your bot's id}/webhooks`) should look like this: -![](https://i.imgur.com/cZfZgK5.png) +#### Social + +```js +const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` \ No newline at end of file From db99c598d135708fc43c3c0e3beb66c563a015ea Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 24 Jun 2025 20:10:56 +0700 Subject: [PATCH 06/48] feat: make webhooks more specific --- .husky/pre-commit | 3 - README.md | 38 ++++++++++++ src/structs/Webhook.ts | 127 ++++++++++++++++++--------------------- src/typings.ts | 18 ++---- tests/mocks/endpoints.ts | 18 +++--- 5 files changed, 109 insertions(+), 95 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 20d0d06..3867a0f 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npm run lint diff --git a/README.md b/README.md index d91dc94..996d506 100644 --- a/README.md +++ b/README.md @@ -156,4 +156,42 @@ const widgetUrl = Topgg.Widget.owner(Topgg.WidgetType.DiscordBot, "5746527517457 ```js const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` + +### Webhooks + +#### Being notified whenever someone voted for your bot + +With express: + +##### CommonJS + +```js +const { Webhook } = require("@top-gg/sdk"); +const express = require("express"); + +const app = express(); +const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); + +app.post("/votes", webhook.voteListener(vote => { + console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); +})); + +app.listen(8080); +``` + +##### ES module + +```js +import { Webhook } from "@top-gg/sdk"; +import express from "express"; + +const app = express(); +const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); + +app.post("/votes", webhook.voteListener(vote => { + console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); +})); + +app.listen(8080); ``` \ No newline at end of file diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index d257ce8..f0d1b54 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -1,10 +1,10 @@ import getBody from "raw-body"; import { Request, Response, NextFunction } from "express"; -import { WebhookPayload } from "../typings"; +import { WebhookVotePayload } from "../typings"; export interface WebhookOptions { /** - * Handles an error created by the function passed to Webhook.listener() + * Handles an error created by the function passed to webhook listeners * * @default console.error */ @@ -22,15 +22,15 @@ export interface WebhookOptions { * const app = express(); * const wh = new Webhook("webhookauth123"); * - * app.post("/dblwebhook", wh.listener((vote) => { + * app.post("/votes", wh.voteListener((vote) => { * // vote is your vote object e.g - * console.log(vote.user); // => 321714991050784770 + * console.log(vote.voterId); // => 321714991050784770 * })); * - * app.listen(80); + * app.listen(8080); * - * // In this situation, your TopGG Webhook dashboard should look like - * // URL = http://your.server.ip:80/dblwebhook + * // In this situation, your Top.gg Webhook dashboard should look like + * // URL = http://your.server.ip:8080/votes * // Authorization: webhookauth123 * ``` * @@ -45,41 +45,41 @@ export class Webhook { * * @param authorization Webhook authorization to verify requests */ - constructor(private authorization?: string, options: WebhookOptions = {}) { + constructor( + private authorization?: string, + options: WebhookOptions = {} + ) { this.options = { - error: options.error ?? console.error, + error: options.error ?? console.error }; } - private _formatIncoming( - body: WebhookPayload & { query: string } - ): WebhookPayload { - const out: WebhookPayload = { ...body }; - if (body?.query?.length > 0) - out.query = Object.fromEntries(new URLSearchParams(body.query)); - return out; + private _formatVotePayload(body: any): WebhookVotePayload { + return { + receiverId: (body.bot ?? body.guild)!, + voterId: body.user, + type: body.type, + isWeekend: body.isWeekend, + query: body.query ?? Object.fromEntries(new URLSearchParams(body.query)) + }; } - private _parseRequest( - req: Request, - res: Response - ): Promise { + private _parseRequest(req: Request, res: Response): Promise { return new Promise((resolve) => { if ( this.authorization && req.headers.authorization !== this.authorization ) - return res.status(403).json({ error: "Unauthorized" }); + return res.status(401).json({ error: "Unauthorized" }); + // parse json + if (req.body) return resolve(req.body); - if (req.body) return resolve(this._formatIncoming(req.body)); getBody(req, {}, (error, body) => { if (error) return res.status(422).json({ error: "Malformed request" }); try { - const parsed = JSON.parse(body.toString("utf8")); - - resolve(this._formatIncoming(parsed)); + resolve(JSON.parse(body.toString("utf8"))); } catch (err) { res.status(400).json({ error: "Invalid body" }); resolve(false); @@ -88,32 +88,10 @@ export class Webhook { }); } - /** - * Listening function for handling webhook requests - * - * @example - * ```js - * app.post("/webhook", wh.listener((vote) => { - * console.log(vote.user); // => 395526710101278721 - * })); - * ``` - * - * @example - * ```js - * // Throwing an error to resend the webhook - * app.post("/webhook/", wh.listener((vote) => { - * // for example, if your bot is offline, you should probably not handle votes and try again - * if (bot.offline) throw new Error('Bot offline'); - * })); - * ``` - * - * @param fn Vote handling function, this function can also throw an error to - * allow for the webhook to resend from Top.gg - * @returns An express request handler - */ - public listener( - fn: ( - payload: WebhookPayload, + private _listener( + formatFn: (data: any) => T, + callbackFn: ( + payload: T, req?: Request, res?: Response, next?: NextFunction @@ -125,10 +103,11 @@ export class Webhook { next: NextFunction ): Promise => { const response = await this._parseRequest(req, res); + if (!response) return; try { - await fn(response, req, res, next); + await callbackFn(formatFn(response), req, res, next); if (!res.headersSent) { res.sendStatus(204); @@ -142,28 +121,36 @@ export class Webhook { } /** - * Middleware function to pass to express, sets req.vote to the payload + * Listening function for handling webhook requests * - * @deprecated Use the new {@link Webhook.listener | .listener()} function * @example * ```js - * app.post("/dblwebhook", wh.middleware(), (req, res) => { - * // req.vote is your payload e.g - * console.log(req.vote.user); // => 395526710101278721 - * }); + * app.post("/webhook", wh.voteListener((vote) => { + * console.log(vote.voterId); // => 395526710101278721 + * })); + * ``` + * + * @example + * ```js + * // Throwing an error to resend the webhook + * app.post("/webhook/", wh.voteListener((vote) => { + * // for example, if your bot is offline, you should probably not handle votes and try again + * if (bot.offline) throw new Error('Bot offline'); + * })); * ``` + * + * @param fn Vote handling function, this function can also throw an error to + * allow for the webhook to resend from Top.gg + * @returns An express request handler */ - public middleware() { - return async ( - req: Request, - res: Response, - next: NextFunction - ): Promise => { - const response = await this._parseRequest(req, res); - if (!response) return; - res.sendStatus(204); - req.vote = response; - next(); - }; + public voteListener( + fn: ( + payload: WebhookVotePayload, + req?: Request, + res?: Response, + next?: NextFunction + ) => void | Promise + ) { + return this._listener(this._formatVotePayload, fn); } } diff --git a/src/typings.ts b/src/typings.ts index 5ca8965..3103186 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -215,13 +215,11 @@ export interface ShortUser { avatar: string; } -export interface WebhookPayload { - /** If webhook is a bot: ID of the bot that received a vote */ - bot?: Snowflake; - /** If webhook is a server: ID of the server that received a vote */ - guild?: Snowflake; - /** ID of the user who voted */ - user: Snowflake; +export interface WebhookVotePayload { + /** The ID of the Discord bot/server that received a vote. */ + receiverId: Snowflake; + /** The ID of the Top.gg user who voted. */ + voterId: Snowflake; /** * The type of the vote (should always be "upvote" except when using the test * button it's "test") @@ -239,9 +237,3 @@ export interface WebhookPayload { } | string; } - -declare module "express" { - export interface Request { - vote?: WebhookPayload; - } -} diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index 868fc18..2e4da0b 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -4,53 +4,53 @@ import { getIdInPath } from '../jest.setup'; export const endpoints = [ { - pattern: '/api/bots', + pattern: '/api/v1/bots', method: 'GET', data: BOTS, requireAuth: true }, { - pattern: '/api/bots/:bot_id', + pattern: '/api/v1/bots/:bot_id', method: 'GET', data: BOT, requireAuth: true, validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/bots/:bot_id', request.path); + const bot_id = getIdInPath('/api/v1/bots/:bot_id', request.path); if (Number(bot_id) === 0) return { statusCode: 404 }; return null; } }, { - pattern: '/api/bots/:bot_id/votes', + pattern: '/api/v1/bots/:bot_id/votes', method: 'GET', data: VOTES, requireAuth: true, validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/bots/:bot_id/votes', request.path); + const bot_id = getIdInPath('/api/v1/bots/:bot_id/votes', request.path); if (Number(bot_id) === 0) return { statusCode: 404 }; return null; } }, { - pattern: '/api/bots/check', + pattern: '/api/v1/bots/check', method: 'GET', data: USER_VOTE, requireAuth: true }, { - pattern: '/api/bots/stats', + pattern: '/api/v1/bots/stats', method: 'GET', data: BOT_STATS, requireAuth: true }, { - pattern: '/api/bots/stats', + pattern: '/api/v1/bots/stats', method: 'POST', data: {}, requireAuth: true }, { - pattern: '/api/weekend', + pattern: '/api/v1/weekend', method: 'GET', data: WEEKEND, requireAuth: true From 327e15b8aaee436b8b94769121ab5afd5248b8e4 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 24 Jun 2025 21:39:18 +0700 Subject: [PATCH 07/48] feat: remove features deprecated by v0 --- README.md | 4 +- src/structs/Api.ts | 119 +++++++---------------------------------- src/structs/Webhook.ts | 33 +++++------- src/typings.ts | 117 +--------------------------------------- 4 files changed, 37 insertions(+), 236 deletions(-) diff --git a/README.md b/README.md index 996d506..ff387eb 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ const { AutoPoster } = require("topgg-autoposter"); const client = Discord.Client(); AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { - console.log("Posted stats to Top.gg!"); + console.log("Successfully posted server count to Top.gg!"); }); ``` @@ -122,7 +122,7 @@ import { AutoPoster } from "topgg-autoposter"; const client = Discord.Client(); AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { - console.log("Posted stats to Top.gg!"); + console.log("Successfully posted server count to Top.gg!"); }); ``` diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 022bb88..93031d7 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -9,7 +9,6 @@ import { Snowflake, BotStats, BotInfo, - UserInfo, BotsResponse, ShortUser, BotsQuery, @@ -21,8 +20,8 @@ import { * @example * ```js * const Topgg = require("@top-gg/sdk"); - * - * const api = new Topgg.Api("Your top.gg token"); + * + * const client = new Topgg.Api(process.env.TOPGG_TOKEN); * ``` * * @link {@link https://topgg.js.org | Library docs} @@ -106,12 +105,12 @@ export class Api extends EventEmitter { } /** - * Post bot stats to Top.gg + * Post your bot's server count to Top.gg * * @example * ```js - * await api.postStats({ - * serverCount: 28199 + * await client.postStats({ + * serverCount: bot.getServerCount() * }); * ``` * @@ -132,31 +131,20 @@ export class Api extends EventEmitter { } /** - * Get your bot's stats + * Get your bot's server count * * @example * ```js - * await api.getStats(); - * // => - * { - * serverCount: 28199, - * shardCount: null, - * shards: [] - * } + * const { serverCount } = await client.getStats(); * ``` * * @returns {BotStats} Your bot's stats */ - public async getStats(_id?: Snowflake): Promise { - if (_id) - console.warn( - "[DeprecationWarning] getStats() no longer needs an ID argument" - ); + public async getStats(): Promise { const result = await this._request("GET", "/bots/stats"); + return { - serverCount: result.server_count, - shardCount: null, - shards: [], + serverCount: result.server_count }; } @@ -165,7 +153,7 @@ export class Api extends EventEmitter { * * @example * ```js - * await api.getBot("461521980492087297"); // returns bot info + * const bot = await client.getBot("461521980492087297"); * ``` * * @param {Snowflake} id Bot ID @@ -176,69 +164,12 @@ export class Api extends EventEmitter { return this._request("GET", `/bots/${id}`); } - /** - * @deprecated No longer supported by Top.gg API v0. - * - * Get user info - * - * @example - * ```js - * await api.getUser("205680187394752512"); - * // => - * user.username; // Xignotic - * ``` - * - * @param {Snowflake} id User ID - * @returns {UserInfo} Info for user - */ - public async getUser(id: Snowflake): Promise { - console.warn( - "[DeprecationWarning] getUser is no longer supported by Top.gg API v0." - ); - - return this._request("GET", `/users/${id}`); - } - /** * Get a list of bots * * @example * ```js - * await api.getBots(); - * // => - * { - * results: [ - * { - * id: "461521980492087297", - * username: "Shiro", - * ...rest of bot object - * } - * ...other shiro knockoffs B) - * ], - * limit: 10, - * offset: 0, - * count: 1, - * total: 1 - * } - * // Restricting fields - * await api.getBots({ - * fields: ["id", "username"], - * }); - * // => - * { - * results: [ - * { - * id: '461521980492087297', - * username: 'Shiro' - * }, - * { - * id: '493716749342998541', - * username: 'Mimu' - * }, - * ... - * ], - * ... - * } + * const bots = await client.getBots(); * ``` * * @param {BotsQuery} query Bot Query @@ -256,21 +187,11 @@ export class Api extends EventEmitter { * * @example * ```js - * await api.getVotes(); - * // => - * [ - * { - * username: 'Xignotic', - * id: '205680187394752512', - * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' - * }, - * { - * username: 'iara', - * id: '395526710101278721', - * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' - * } - * ...more - * ] + * // First page + * const voters1 = await client.getVotes(); + * + * // Subsequent pages + * const voters2 = await client.getVotes(2); * ``` * * @param {number} [page] The page number. Each page can only have at most 100 voters. @@ -285,8 +206,7 @@ export class Api extends EventEmitter { * * @example * ```js - * await api.hasVoted("205680187394752512"); - * // => true/false + * const hasVoted = await client.hasVoted("205680187394752512"); * ``` * * @param {Snowflake} id User ID @@ -304,8 +224,7 @@ export class Api extends EventEmitter { * * @example * ```js - * await api.isWeekend(); - * // => true/false + * const isWeekend = await client.isWeekend(); * ``` * * @returns {boolean} Whether the multiplier is active diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index f0d1b54..4b8e1e7 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -16,22 +16,17 @@ export interface WebhookOptions { * * @example * ```js - * const express = require("express"); * const { Webhook } = require("@top-gg/sdk"); - * + * const express = require("express"); + * * const app = express(); - * const wh = new Webhook("webhookauth123"); - * - * app.post("/votes", wh.voteListener((vote) => { - * // vote is your vote object e.g - * console.log(vote.voterId); // => 321714991050784770 + * const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); + * + * app.post("/votes", webhook.voteListener(vote => { + * console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); * })); - * + * * app.listen(8080); - * - * // In this situation, your Top.gg Webhook dashboard should look like - * // URL = http://your.server.ip:8080/votes - * // Authorization: webhookauth123 * ``` * * @link {@link https://docs.top.gg/resources/webhooks/#schema | Webhook Data Schema} @@ -43,7 +38,7 @@ export class Webhook { /** * Create a new webhook client instance * - * @param authorization Webhook authorization to verify requests + * @param {?string} authorization Webhook authorization to verify requests */ constructor( private authorization?: string, @@ -58,7 +53,7 @@ export class Webhook { return { receiverId: (body.bot ?? body.guild)!, voterId: body.user, - type: body.type, + isTest: body.type === "test", isWeekend: body.isWeekend, query: body.query ?? Object.fromEntries(new URLSearchParams(body.query)) }; @@ -125,21 +120,21 @@ export class Webhook { * * @example * ```js - * app.post("/webhook", wh.voteListener((vote) => { - * console.log(vote.voterId); // => 395526710101278721 + * app.post("/votes", webhook.voteListener(vote => { + * console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); * })); * ``` * * @example * ```js * // Throwing an error to resend the webhook - * app.post("/webhook/", wh.voteListener((vote) => { - * // for example, if your bot is offline, you should probably not handle votes and try again + * app.post("/votes", webhook.voteListener(vote => { + * // For example, if your bot is offline, you should probably not handle votes and try again. * if (bot.offline) throw new Error('Bot offline'); * })); * ``` * - * @param fn Vote handling function, this function can also throw an error to + * @param {(payload: WebhookVotePayload, req?: Request, res?: Response, next?: NextFunction) => void | Promise} fn Vote handling function, this function can also throw an error to * allow for the webhook to resend from Top.gg * @returns An express request handler */ diff --git a/src/typings.ts b/src/typings.ts index 3103186..8bac9c4 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -16,32 +16,8 @@ export interface BotInfo { clientid: Snowflake; /** The username of the bot */ username: string; - /** - * The discriminator of the bot - * - * @deprecated No longer supported by Top.gg API v0. - */ - discriminator: string; /** The bot's avatar */ avatar: string; - /** - * The cdn hash of the bot's avatar if the bot has none - * - * @deprecated No longer supported by Top.gg API v0. - */ - defAvatar: string; - /** - * The URL for the banner image - * - * @deprecated No longer supported by Top.gg API v0. - */ - bannerUrl?: string; - /** - * The library of the bot - * - * @deprecated No longer supported by Top.gg API v0. - */ - lib: string; /** The prefix of the bot */ prefix: string; /** The short description of the bot */ @@ -58,34 +34,16 @@ export interface BotInfo { github?: string; /** The owners of the bot. First one in the array is the main owner */ owners: Snowflake[]; - /** - * The guilds featured on the bot page - * - * @deprecated No longer supported by Top.gg API v0. - */ - guilds: Snowflake[]; /** The custom bot invite url of the bot */ invite?: string; /** The date when the bot was submitted (in ISO 8601) */ date: string; - /** - * The certified status of the bot - * - * @deprecated No longer supported by Top.gg API v0. - */ - certifiedBot: boolean; /** The vanity url of the bot */ vanity?: string; /** The amount of votes the bot has */ points: number; /** The amount of votes the bot has this month */ monthlyPoints: number; - /** - * The guild id for the donatebot setup - * - * @deprecated No longer supported by Top.gg API v0. - */ - donatebotguildid: Snowflake; /** The amount of servers the bot is in based on posted stats */ server_count?: number; /** The bot's reviews on Top.gg */ @@ -100,70 +58,6 @@ export interface BotInfo { export interface BotStats { /** The amount of servers the bot is in */ serverCount?: number; - /** - * The amount of servers the bot is in per shard. Always present but can be - * empty. (Only when receiving stats) - * - * @deprecated No longer supported by Top.gg API v0. - */ - shards?: number[]; - /** - * The shard ID to post as (only when posting) - * - * @deprecated No longer supported by Top.gg API v0. - */ - shardId?: number; - /** - * The amount of shards a bot has - * - * @deprecated No longer supported by Top.gg API v0. - */ - shardCount?: number | null; -} - -/** - * @deprecated No longer supported by Top.gg API v0. - */ -export interface UserInfo { - /** The id of the user */ - id: Snowflake; - /** The username of the user */ - username: string; - /** The discriminator of the user */ - discriminator: string; - /** The user's avatar url */ - avatar: string; - /** The cdn hash of the user's avatar if the user has none */ - defAvatar: string; - /** The bio of the user */ - bio?: string; - /** The banner image url of the user */ - banner?: string; - /** The social usernames of the user */ - social: { - /** The youtube channel id of the user */ - youtube?: string; - /** The reddit username of the user */ - reddit?: string; - /** The twitter username of the user */ - twitter?: string; - /** The instagram username of the user */ - instagram?: string; - /** The github username of the user */ - github?: string; - }; - /** The custom hex color of the user */ - color: string; - /** The supporter status of the user */ - supporter: boolean; - /** The certified status of the user */ - certifiedDev: boolean; - /** The mod status of the user */ - mod: boolean; - /** The website moderator status of the user */ - webMod: boolean; - /** The admin status of the user */ - admin: boolean; } export interface BotsQuery { @@ -205,12 +99,6 @@ export interface ShortUser { id: Snowflake; /** User's username */ username: string; - /** - * User's discriminator - * - * @deprecated No longer supported by Top.gg API v0. - */ - discriminator: string; /** User's avatar url */ avatar: string; } @@ -221,10 +109,9 @@ export interface WebhookVotePayload { /** The ID of the Top.gg user who voted. */ voterId: Snowflake; /** - * The type of the vote (should always be "upvote" except when using the test - * button it's "test") + * Whether this vote is just a test done from the page settings. */ - type: string; + isTest: boolean; /** * Whether the weekend multiplier is in effect, meaning users votes count as * two From 1a03b2779bbce064b0116d80662ff3c6963b5e48 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 4 Jul 2025 20:42:53 +0700 Subject: [PATCH 08/48] feat: remove BotStats --- README.md | 10 ++++------ src/structs/Api.ts | 35 ++++++++++++----------------------- src/typings.ts | 5 ----- tests/Api.test.ts | 32 +++++++++++--------------------- 4 files changed, 27 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index ff387eb..61bf2ff 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,13 @@ const bots = await client.getBots(); #### First page ```js -const voters = await client.getVotes(); +const voters = await client.getVoters(); ``` #### Subsequent pages ```js -const voters = await client.getVotes(2); +const voters = await client.getVoters(2); ``` ### Check if a user has voted for your bot @@ -71,15 +71,13 @@ const hasVoted = await client.hasVoted("205680187394752512"); ### Getting your bot's server count ```js -const { serverCount } = await client.getStats(); +const serverCount = await client.getServerCount(); ``` ### Posting your bot's server count ```js -await client.postStats({ - serverCount: bot.getServerCount() -}); +await client.postServerCount(bot.getServerCount()); ``` ### Automatically posting your bot's server count every few minutes diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 93031d7..169be18 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -7,7 +7,6 @@ import { STATUS_CODES } from "http"; import { APIOptions, Snowflake, - BotStats, BotInfo, BotsResponse, ShortUser, @@ -109,25 +108,19 @@ export class Api extends EventEmitter { * * @example * ```js - * await client.postStats({ - * serverCount: bot.getServerCount() - * }); + * await client.postServerCount(bot.getServerCount()); * ``` * - * @param {object} stats Stats object - * @param {number} stats.serverCount Server count - * @returns {BotStats} Passed object + * @param {number} serverCount Server count */ - public async postStats(stats: BotStats): Promise { - if ((stats?.serverCount ?? 0) <= 0) throw new Error("Missing server count"); + public async postServerCount(serverCount: number): Promise { + if ((serverCount ?? 0) <= 0) throw new Error("Missing server count"); /* eslint-disable camelcase */ await this._request("POST", "/bots/stats", { - server_count: stats.serverCount, + server_count: serverCount, }); /* eslint-enable camelcase */ - - return stats; } /** @@ -135,17 +128,13 @@ export class Api extends EventEmitter { * * @example * ```js - * const { serverCount } = await client.getStats(); + * const serverCount = await client.getServerCount(); * ``` * - * @returns {BotStats} Your bot's stats + * @returns {number} Your bot's server count */ - public async getStats(): Promise { - const result = await this._request("GET", "/bots/stats"); - - return { - serverCount: result.server_count - }; + public async getServerCount(): Promise { + return (await this._request("GET", "/bots/stats")).server_count; } /** @@ -188,16 +177,16 @@ export class Api extends EventEmitter { * @example * ```js * // First page - * const voters1 = await client.getVotes(); + * const voters1 = await client.getVoters(); * * // Subsequent pages - * const voters2 = await client.getVotes(2); + * const voters2 = await client.getVoters(2); * ``` * * @param {number} [page] The page number. Each page can only have at most 100 voters. * @returns {ShortUser[]} Array of unique users who've voted */ - public async getVotes(page?: number): Promise { + public async getVoters(page?: number): Promise { return this._request("GET", `/bots/${this.options.id}/votes`, { page: page ?? 1 }); } diff --git a/src/typings.ts b/src/typings.ts index 8bac9c4..09b7a05 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -55,11 +55,6 @@ export interface BotInfo { }; } -export interface BotStats { - /** The amount of servers the bot is in */ - serverCount?: number; -} - export interface BotsQuery { /** The amount of bots to return. Max. 500 */ limit?: number; diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 0d6dda3..6073be1 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -5,29 +5,19 @@ import { BOT, BOT_STATS, VOTES } from './mocks/data'; /* mock token */ const client = new Api('.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.'); -describe('API postStats test', () => { - it('postStats without server count should throw error', async () => { - await expect(client.postStats({ shardCount: 0 })).rejects.toThrow(Error); +describe('API postServerCount test', () => { + it('postServerCount with invalid negative server count should throw error', () => { + expect(client.postServerCount(-1)).rejects.toThrow(Error); }); - it('postStats with invalid negative server count should throw error', () => { - expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); - }); - - it('postStats should return 200', async () => { - await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( - Object - ); + it('postServerCount should return 200', async () => { + await expect(client.postServerCount(1)).resolves.toBeUndefined(); }); }); -describe('API getStats test', () => { - it('getStats should return 200 when bot is found', async () => { - expect(client.getStats()).resolves.toStrictEqual({ - serverCount: BOT_STATS.server_count, - shardCount: BOT_STATS.shard_count, - shards: BOT_STATS.shards - }); +describe('API getServerCount test', () => { + it('getServerCount should return 200 when bot is found', async () => { + expect(client.getServerCount()).resolves.toStrictEqual(BOT_STATS.server_count); }); }); @@ -45,9 +35,9 @@ describe('API getBot test', () => { }); }); -describe('API getVotes test', () => { - it('getVotes should return 200 when token is provided', () => { - expect(client.getVotes()).resolves.toEqual(VOTES); +describe('API getVoters test', () => { + it('getVoters should return 200 when token is provided', () => { + expect(client.getVoters()).resolves.toEqual(VOTES); }); }); From ed43118db1a386c4c6f533154c7eb4e469a7eb61 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 17 Jul 2025 02:49:23 +0700 Subject: [PATCH 09/48] deps: remove the dependence on @top-gg/eslint-config --- .eslintrc.js | 7 ---- .husky/pre-commit | 2 +- .npmignore | 2 +- README.md | 17 +++++++++ eslint.config.js | 81 ++++++++++++++++++++++++++++++++++++++++++ package.json | 42 +++++++++++----------- src/structs/Webhook.ts | 2 +- 7 files changed, 122 insertions(+), 31 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 eslint.config.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index fbe2908..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - ignorePatterns: ["node_modules/*", "docs/*", "dist/*"], - extends: "@top-gg/eslint-config", - parserOptions: { - project: "./tsconfig.json", - } -}; diff --git a/.husky/pre-commit b/.husky/pre-commit index 3867a0f..a845b85 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npm run lint +npm run lint \ No newline at end of file diff --git a/.npmignore b/.npmignore index 3f76e69..7bd816b 100644 --- a/.npmignore +++ b/.npmignore @@ -5,7 +5,7 @@ logs/ *.log !dist/ -.eslintrc.json +eslint.config.js .gitattributes .gitignore .github/ diff --git a/README.md b/README.md index 61bf2ff..00fddf5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,23 @@ The community-maintained Node.js library for Top.gg. +## Chapters + +- [Installation](#installation) +- [Setting up](#setting-up) +- [Usage](#usage) + - [Getting a bot](#getting-a-bot) + - [Getting several bots](#getting-several-bots) + - [Getting your bot's voters](#getting-your-bots-voters) + - [Check if a user has voted for your bot](#check-if-a-user-has-voted-for-your-bot) + - [Getting your bot's server count](#getting-your-bots-server-count) + - [Posting your bot's server count](#posting-your-bots-server-count) + - [Automatically posting your bot's server count every few minutes]#automatically-posting-your-bots-server-count-every-few-minutes) + - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) + - [Generating widget URLs](#generating-widget-urls) + - [Webhooks](#webhooks) + - [Being notified whenever someone voted for your bot](#being-notified-whenever-someone-voted-for-your-bot) + ## Installation ### NPM diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..7e89370 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,81 @@ +const js = require('@eslint/js'); +const ts = require("@typescript-eslint/eslint-plugin"); + +const tsPlugin = require('@typescript-eslint/eslint-plugin'); +const jestPlugin = require('eslint-plugin-jest'); + +module.exports = [ + { + ignores: ["node_modules/*", "docs/*", "dist/*"] + }, + { + ...js.configs.recommended, + files: ["src/**/*.ts"], + languageOptions: { + parser: require('@typescript-eslint/parser'), + parserOptions: { + project: './tsconfig.json', + }, + globals: { + es6: true, + browser: true, + node: true, + jest: true + } + }, + plugins: { + "@typescript-eslint": tsPlugin, + jest: jestPlugin + }, + rules: { + ...ts.configs.recommended.rules, + semi: "error", + "no-unreachable-loop": "warn", + "no-unsafe-optional-chaining": "warn", + eqeqeq: "error", + "no-alert": "error", + "prefer-spread": "error", + "no-duplicate-imports": "warn", + "no-eval": "error", + "no-implied-eval": "error", + "no-extend-native": "warn", + "no-new-wrappers": "error", + "no-proto": "error", + "no-script-url": "error", + "no-self-compare": "warn", + "no-useless-catch": "warn", + "no-throw-literal": "error", + "no-var": "warn", + "no-labels": "error", + "no-undefined": "off", + "no-new-object": "error", + "no-multi-assign": "warn", + "prefer-const": "warn", + "prefer-numeric-literals": "warn", + "prefer-object-spread": "error", + "prefer-rest-params": "error", + "prefer-exponentiation-operator": "error", + "no-lonely-if": "error", + radix: "warn", + camelcase: "warn", + "new-cap": "error", + quotes: ["warn", "double", { allowTemplateLiterals: true }], + "no-void": "error", + "spaced-comment": ["warn", "always"], + "eol-last": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/prefer-optional-chain": "error", + "@typescript-eslint/prefer-for-of": "error", + "@typescript-eslint/no-namespace": [ + "error", + { allowDefinitionFiles: true } + ] + } + }, + { + files: ["*.browser.js"], + env: { + browser: true + } + } +]; diff --git a/package.json b/package.json index 94c010e..78a610d 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "build:ci": "npm i --include=dev && tsc", "docs": "typedoc", "prepublishOnly": "npm run build:ci", - "lint": "eslint src/**/*.ts", - "lint:ci": "eslint --output-file eslint_report.json --format json src/**/*.ts", + "lint": "eslint", + "lint:ci": "eslint --output-file eslint_report.json --format json", "prepare": "npx husky install" }, "repository": { @@ -25,27 +25,27 @@ }, "homepage": "https://topgg.js.org", "devDependencies": { - "@top-gg/eslint-config": "^0.0.4", - "@types/express": "^4.17.17", - "@types/jest": "^29.5.4", - "@types/node": "^20.5.9", - "@typescript-eslint/eslint-plugin": "^6.6.0", - "@typescript-eslint/parser": "^6.6.0", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-jest": "^27.2.3", - "express": "^4.18.2", - "husky": "^8.0.3", - "jest": "^29.6.4", - "lint-staged": "^14.0.1", - "prettier": "^3.0.3", - "ts-jest": "^29.1.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@eslint/js": "^9.31.0", + "@types/express": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.14", + "@typescript-eslint/eslint-plugin": "^8.37.0", + "@typescript-eslint/parser": "^8.37.0", + "eslint": "^9.31.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-jest": "^29.0.1", + "express": "^5.1.0", + "husky": "^9.1.7", + "jest": "^30.0.4", + "lint-staged": "^16.1.2", + "prettier": "^3.6.2", + "ts-jest": "^29.4.0", + "typedoc": "^0.28.7", + "typescript": "^5.8.3" }, "dependencies": { - "raw-body": "^2.5.2", - "undici": "^5.23.0" + "raw-body": "^3.0.0", + "undici": "^7.11.0" }, "types": "./dist/index.d.ts" } diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index 4b8e1e7..f39656d 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -75,7 +75,7 @@ export class Webhook { try { resolve(JSON.parse(body.toString("utf8"))); - } catch (err) { + } catch { res.status(400).json({ error: "Invalid body" }); resolve(false); } From b27d29491d9abd7fc0c832e9b807dd90f203a389 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 10 Sep 2025 20:37:34 +0700 Subject: [PATCH 10/48] feat: adapt to v1 --- README.md | 41 +++++++++++--- package.json | 1 + src/structs/Api.ts | 117 +++++++++++++++++++++++++++++++++------ src/structs/Widget.ts | 12 ++-- src/typings.ts | 25 ++++----- src/utils/ApiError.ts | 17 ++++-- tests/Api.test.ts | 46 +++++++++------ tests/mocks/data.ts | 13 +++++ tests/mocks/endpoints.ts | 37 +++++++++---- 9 files changed, 231 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 00fddf5..2ced25a 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ The community-maintained Node.js library for Top.gg. - [Usage](#usage) - [Getting a bot](#getting-a-bot) - [Getting several bots](#getting-several-bots) - - [Getting your bot's voters](#getting-your-bots-voters) - - [Check if a user has voted for your bot](#check-if-a-user-has-voted-for-your-bot) + - [Getting your project's voters](#getting-your-projects-voters) + - [Check if a user has voted for your project](#check-if-a-user-has-voted-for-your-project) - [Getting your bot's server count](#getting-your-bots-server-count) - [Posting your bot's server count](#posting-your-bots-server-count) - [Automatically posting your bot's server count every few minutes]#automatically-posting-your-bots-server-count-every-few-minutes) - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) - [Generating widget URLs](#generating-widget-urls) - [Webhooks](#webhooks) - - [Being notified whenever someone voted for your bot](#being-notified-whenever-someone-voted-for-your-bot) + - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) ## Installation @@ -65,7 +65,7 @@ const bot = await client.getBot("461521980492087297"); const bots = await client.getBots(); ``` -### Getting your bot's voters +### Getting your project's voters #### First page @@ -79,22 +79,45 @@ const voters = await client.getVoters(); const voters = await client.getVoters(2); ``` -### Check if a user has voted for your bot +### Getting your project's vote information of a user ```js -const hasVoted = await client.hasVoted("205680187394752512"); +const vote = await client.getVote("8226924471638491136"); ``` ### Getting your bot's server count ```js -const serverCount = await client.getServerCount(); +const serverCount = await client.getBotServerCount(); ``` ### Posting your bot's server count ```js -await client.postServerCount(bot.getServerCount()); +await client.postBotServerCount(bot.getServerCount()); +``` + +### Posting your bot's application commands list + +```js +// Discord.js: +const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); + +// Eris: +const commands = await bot.getCommands(); + +// Discordeno: +import { getApplicationCommands } from "discordeno"; + +const commands = await getApplicationCommands(bot); + +// Harmony: +const commands = await bot.interactions.commands.all(); + +// Oceanic: +const commands = await bot.application.getGlobalCommands(); + +await client.postBotCommands(commands); ``` ### Automatically posting your bot's server count every few minutes @@ -175,7 +198,7 @@ const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745 ### Webhooks -#### Being notified whenever someone voted for your bot +#### Being notified whenever someone voted for your project With express: diff --git a/package.json b/package.json index 78a610d..6ceabb1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/node": "^24.0.14", "@typescript-eslint/eslint-plugin": "^8.37.0", "@typescript-eslint/parser": "^8.37.0", + "discord-api-types": "^0.38.23", "eslint": "^9.31.0", "eslint-config-prettier": "^10.1.5", "eslint-plugin-jest": "^29.0.1", diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 169be18..b14f7c2 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -1,6 +1,7 @@ -import { request, type Dispatcher } from "undici"; +import type { APIApplicationCommand } from "discord-api-types/v10"; import type { IncomingHttpHeaders } from "undici/types/header"; -import ApiError from "../utils/ApiError"; +import { request, type Dispatcher } from "undici"; +import TopGGAPIError from "../utils/ApiError"; import { EventEmitter } from "events"; import { STATUS_CODES } from "http"; @@ -11,6 +12,7 @@ import { BotsResponse, ShortUser, BotsQuery, + Vote, } from "../typings"; /** @@ -28,6 +30,7 @@ import { */ export class Api extends EventEmitter { private options: APIOptions; + private legacy: boolean; /** * Create Top.gg API instance @@ -45,10 +48,11 @@ export class Api extends EventEmitter { } try { - const tokenData = atob(tokenSegments[1]); - const tokenId = JSON.parse(tokenData).id; + const tokenData = JSON.parse(atob(tokenSegments[1])); + const tokenId = tokenData.id; options.id ??= tokenId; + this.legacy = !("_t" in tokenData); } catch { throw new Error( "Invalid API token state, this should not happen! Please report!" @@ -67,10 +71,10 @@ export class Api extends EventEmitter { body?: Record ): Promise { const headers: IncomingHttpHeaders = {}; - if (this.options.token) headers["authorization"] = this.options.token; + if (this.options.token) headers["authorization"] = `Bearer ${this.options.token}`; if (method !== "GET") headers["content-type"] = "application/json"; - let url = `https://top.gg/api/v1${path}`; + let url = `https://top.gg/api${path}`; if (body && method === "GET") url += `?${new URLSearchParams(body)}`; @@ -80,23 +84,23 @@ export class Api extends EventEmitter { body: body && method !== "GET" ? JSON.stringify(body) : undefined, }); - let responseBody; + let responseBody: string | object | undefined; if ( (response.headers["content-type"] as string)?.startsWith( "application/json" ) ) { - responseBody = await response.body.json(); + responseBody = await response.body.json() as object; } else { responseBody = await response.body.text(); } if (response.statusCode < 200 || response.statusCode > 299) { - throw new ApiError( + throw new TopGGAPIError( response.statusCode, STATUS_CODES[response.statusCode] ?? "", - response + responseBody ); } @@ -104,16 +108,51 @@ export class Api extends EventEmitter { } /** - * Post your bot's server count to Top.gg + * Updates the application commands list in your Discord bot's Top.gg page. + * + * @example + * ```js + * // Discord.js: + * const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); + * + * // Eris: + * const commands = await bot.getCommands(); + * + * // Discordeno: + * import { getApplicationCommands } from "discordeno"; + * + * const commands = await getApplicationCommands(bot); + * + * // Harmony: + * const commands = await bot.interactions.commands.all(); + * + * // Oceanic: + * const commands = await bot.application.getGlobalCommands(); + * + * await client.postBotCommands(commands); + * ``` + * + * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON dicts. This cannot be empty. + */ + public async postBotCommands(commands: APIApplicationCommand[]): Promise { + if (this.legacy) { + throw new Error("This endpoint is inaccessible with legacy API tokens."); + } + + await this._request("POST", "/v1/projects/@me/commands", commands); + } + + /** + * Post your Discord bot's server count to Top.gg * * @example * ```js - * await client.postServerCount(bot.getServerCount()); + * await client.postBotServerCount(bot.getServerCount()); * ``` * * @param {number} serverCount Server count */ - public async postServerCount(serverCount: number): Promise { + public async postBotServerCount(serverCount: number): Promise { if ((serverCount ?? 0) <= 0) throw new Error("Missing server count"); /* eslint-disable camelcase */ @@ -124,16 +163,16 @@ export class Api extends EventEmitter { } /** - * Get your bot's server count + * Get your Discord bot's server count * * @example * ```js - * const serverCount = await client.getServerCount(); + * const serverCount = await client.getBotServerCount(); * ``` * * @returns {number} Your bot's server count */ - public async getServerCount(): Promise { + public async getBotServerCount(): Promise { return (await this._request("GET", "/bots/stats")).server_count; } @@ -172,7 +211,7 @@ export class Api extends EventEmitter { } /** - * Get recent unique users who've voted + * Get recent 100 unique voters * * @example * ```js @@ -184,13 +223,15 @@ export class Api extends EventEmitter { * ``` * * @param {number} [page] The page number. Each page can only have at most 100 voters. - * @returns {ShortUser[]} Array of unique users who've voted + * @returns {ShortUser[]} Array of 100 unique voters */ public async getVoters(page?: number): Promise { return this._request("GET", `/bots/${this.options.id}/votes`, { page: page ?? 1 }); } /** + * @deprecated Use a v1 API token with `getVote()` instead. + * * Get whether or not a user has voted in the last 12 hours * * @example @@ -203,11 +244,51 @@ export class Api extends EventEmitter { */ public async hasVoted(id: Snowflake): Promise { if (!id) throw new Error("Missing ID"); + + console.warn("`hasVoted()` is deprecated. Use a v1 API token with `getVote()` instead."); + return this._request("GET", "/bots/check", { userId: id }).then( (x) => !!x.voted ); } + /** + * Get the latest vote information of a Top.gg user on your project. + * + * @example + * ```js + * const vote = await client.getVote("8226924471638491136"); + * ``` + * + * @param {Snowflake} id The Top.gg user's ID. + * @returns {?Vote} The user's latest vote information on your project or null if the user has not voted for your project in the past 12 hours. + */ + public async getVote(id: Snowflake): Promise { + if (!id) throw new Error("Missing ID"); + + if (this.legacy) { + throw new Error("This endpoint is inaccessible with legacy API tokens."); + } + + try { + const response = await this._request("GET", `/v1/projects/@me/votes/${id}`); + + return { + votedAt: response.created_at, + expiresAt: response.expires_at, + weight: response.weight + }; + } catch (err) { + const topggError = err as TopGGAPIError; + + if ((topggError?.body as { title?: string })?.title === "Vote expired") { + return null; + } + + throw err; + } + } + /** * Whether or not the weekend multiplier is active * diff --git a/src/structs/Widget.ts b/src/structs/Widget.ts index 084d77a..011facc 100644 --- a/src/structs/Widget.ts +++ b/src/structs/Widget.ts @@ -1,6 +1,6 @@ import { Snowflake } from "../typings"; -const BASE_URL: string = "https://top.gg/api/v1"; +const BASE_URL: string = "https://top.gg/api/v1/widgets"; /** * Widget type. @@ -22,7 +22,7 @@ export class Widget { * @returns {string} The widget URL. */ public static large(ty: WidgetType, id: Snowflake): string { - return `${BASE_URL}/widgets/large/${ty}/${id}`; + return `${BASE_URL}/large/${ty}/${id}`; } /** @@ -33,18 +33,18 @@ export class Widget { * @returns {string} The widget URL. */ public static votes(ty: WidgetType, id: Snowflake): string { - return `${BASE_URL}/widgets/small/votes/${ty}/${id}`; + return `${BASE_URL}/small/votes/${ty}/${id}`; } /** - * Generates a small widget URL for displaying an entity's owner. + * Generates a small widget URL for displaying a project's owner. * * @param {WidgetType} ty The widget type. * @param {Snowflake} id The ID. * @returns {string} The widget URL. */ public static owner(ty: WidgetType, id: Snowflake): string { - return `${BASE_URL}/widgets/small/owner/${ty}/${id}`; + return `${BASE_URL}/small/owner/${ty}/${id}`; } /** @@ -55,6 +55,6 @@ export class Widget { * @returns {string} The widget URL. */ public static social(ty: WidgetType, id: Snowflake): string { - return `${BASE_URL}/widgets/small/social/${ty}/${id}`; + return `${BASE_URL}/small/social/${ty}/${id}`; } } diff --git a/src/typings.ts b/src/typings.ts index 09b7a05..7dc09bf 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -1,8 +1,8 @@ export interface APIOptions { - /** Top.gg token */ + /** Top.gg API token */ token?: string; - /** Discord bot ID */ + /** Client ID to use */ id?: string; } @@ -60,16 +60,6 @@ export interface BotsQuery { limit?: number; /** Amount of bots to skip */ offset?: number; - /** - * A search string in the format of "field: value field2: value2" - * - * @deprecated No longer supported by Top.gg API v1. - */ - search?: - | { - [key in keyof BotInfo]: string; - } - | string; /** Sorts results from a specific criteria. Results will always be descending. */ sort?: "monthlyPoints" | "id" | "date"; /** A list of fields to show. */ @@ -89,6 +79,15 @@ export interface BotsResponse { total: number; } +export interface Vote { + /** When this vote was cast */ + votedAt?: string; + /** When this vote expires and the user is required to vote again */ + expiresAt?: string; + /** This vote's weight */ + weight?: number; +} + export interface ShortUser { /** User's ID */ id: Snowflake; @@ -99,7 +98,7 @@ export interface ShortUser { } export interface WebhookVotePayload { - /** The ID of the Discord bot/server that received a vote. */ + /** The ID of the project that received a vote. */ receiverId: Snowflake; /** The ID of the Top.gg user who voted. */ voterId: Snowflake; diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index b4df924..5a8bd1c 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -1,5 +1,3 @@ -import type { Dispatcher } from "undici"; - const tips = { 401: "You need a token for this endpoint", 403: "You don't have access to this endpoint", @@ -7,14 +5,21 @@ const tips = { /** API Error */ export default class TopGGAPIError extends Error { - /** Possible response from Request */ - public response?: Dispatcher.ResponseData; - constructor(code: number, text: string, response: Dispatcher.ResponseData) { + /** Response status code */ + public statusCode: number; + + /** Possible response body from Response */ + public body?: string | object; + + constructor(code: number, text: string, body?: string | object) { if (code in tips) { super(`${code} ${text} (${tips[code as keyof typeof tips]})`); } else { super(`${code} ${text}`); } - this.response = response; + + this.statusCode = code; + this.message = tips[code as keyof typeof tips] ?? text; + this.body = body; } } diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 6073be1..6f9968d 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,23 +1,37 @@ import { Api } from '../src/index'; import ApiError from '../src/utils/ApiError'; -import { BOT, BOT_STATS, VOTES } from './mocks/data'; +import { BOT, BOT_STATS, VOTE, VOTES } from './mocks/data'; /* mock token */ -const client = new Api('.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.'); +const client = new Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); -describe('API postServerCount test', () => { - it('postServerCount with invalid negative server count should throw error', () => { - expect(client.postServerCount(-1)).rejects.toThrow(Error); +describe('API postBotCommands test', () => { + it('postBotCommands should work', () => { + expect(client.postBotCommands([{ + id: '1', + type: 1, + application_id: '1', + name: 'test', + description: 'command description', + default_member_permissions: '', + version: '1' + }])).resolves.toBeUndefined(); + }); +}); + +describe('API postBotServerCount test', () => { + it('postBotServerCount with invalid negative server count should throw error', () => { + expect(client.postBotServerCount(-1)).rejects.toThrow(Error); }); - it('postServerCount should return 200', async () => { - await expect(client.postServerCount(1)).resolves.toBeUndefined(); + it('postBotServerCount should return 200', async () => { + await expect(client.postBotServerCount(1)).resolves.toBeUndefined(); }); }); -describe('API getServerCount test', () => { - it('getServerCount should return 200 when bot is found', async () => { - expect(client.getServerCount()).resolves.toStrictEqual(BOT_STATS.server_count); +describe('API getBotServerCount test', () => { + it('getBotServerCount should return 200 when bot is found', async () => { + expect(client.getBotServerCount()).resolves.toStrictEqual(BOT_STATS.server_count); }); }); @@ -37,17 +51,17 @@ describe('API getBot test', () => { describe('API getVoters test', () => { it('getVoters should return 200 when token is provided', () => { - expect(client.getVoters()).resolves.toEqual(VOTES); + expect(client.getVoters()).resolves.toStrictEqual(VOTES); }); }); -describe('API hasVoted test', () => { - it('hasVoted should return 200 when token is provided', () => { - expect(client.hasVoted('1')).resolves.toBe(true); +describe('API getVote test', () => { + it('getVote should return 200 when token is provided', () => { + expect(client.getVote('1')).resolves.toStrictEqual(VOTE); }); - it('hasVoted should throw error when no id is provided', () => { - expect(client.hasVoted('')).rejects.toThrow(Error); + it('getVote should throw error when no id is provided', () => { + expect(client.getVote('')).rejects.toThrow(Error); }); }); diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index 65ec229..c158848 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -31,6 +31,19 @@ export const BOTS = { results: [BOT], } +export const RAW_VOTE = { + created_at: "2025-09-09T08:55:16.218761+00:00", + expires_at: "2025-09-09T20:55:16.218761+00:00", + weight: 1 +}; + +export const VOTE = { + voted: true, + votedAt: "2025-09-09T08:55:16.218761+00:00", + expiresAt: "2025-09-09T20:55:16.218761+00:00", + weight: 1 +}; + // https://docs.top.gg/api/bot/#last-1000-votes export const VOTES = [ { diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index 2e4da0b..c7e8b62 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -1,58 +1,75 @@ import { MockInterceptor } from 'undici/types/mock-interceptor'; -import { BOT, BOTS, BOT_STATS, USER_VOTE, VOTES, WEEKEND } from './data'; +import { BOT, BOTS, BOT_STATS, RAW_VOTE, USER_VOTE, VOTES, WEEKEND } from './data'; import { getIdInPath } from '../jest.setup'; export const endpoints = [ { - pattern: '/api/v1/bots', + pattern: '/api/bots', method: 'GET', data: BOTS, requireAuth: true }, { - pattern: '/api/v1/bots/:bot_id', + pattern: '/api/bots/:bot_id', method: 'GET', data: BOT, requireAuth: true, validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/v1/bots/:bot_id', request.path); + const bot_id = getIdInPath('/api/bots/:bot_id', request.path); if (Number(bot_id) === 0) return { statusCode: 404 }; return null; } }, { - pattern: '/api/v1/bots/:bot_id/votes', + pattern: '/api/bots/:bot_id/votes', method: 'GET', data: VOTES, requireAuth: true, validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/v1/bots/:bot_id/votes', request.path); + const bot_id = getIdInPath('/api/bots/:bot_id/votes', request.path); if (Number(bot_id) === 0) return { statusCode: 404 }; return null; } }, { - pattern: '/api/v1/bots/check', + pattern: '/api/bots/check', method: 'GET', data: USER_VOTE, requireAuth: true }, { - pattern: '/api/v1/bots/stats', + pattern: '/api/bots/stats', method: 'GET', data: BOT_STATS, requireAuth: true }, { - pattern: '/api/v1/bots/stats', + pattern: '/api/bots/stats', method: 'POST', data: {}, requireAuth: true }, { - pattern: '/api/v1/weekend', + pattern: '/api/weekend', method: 'GET', data: WEEKEND, requireAuth: true + }, + { + pattern: '/api/v1/projects/@me/votes/:user_id', + method: 'GET', + data: RAW_VOTE, + requireAuth: true, + validate: (request: MockInterceptor.MockResponseCallbackOptions) => { + const user_id = getIdInPath('/api/v1/projects/@me/votes/:user_id', request.path); + if (Number(user_id) === 0) return { statusCode: 404 }; + return null; + } + }, + { + pattern: '/api/v1/projects/@me/commands', + method: 'POST', + data: {}, + requireAuth: true } ] \ No newline at end of file From 1b2027c880016fc4c3b34e085066daec6cccdabd Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 12 Sep 2025 18:17:12 +0700 Subject: [PATCH 11/48] *: minor tweaks --- README.md | 46 +++++++++++++++++++++++++++++++++++++-------- package.json | 2 +- src/structs/Api.ts | 26 ++++++++++++++++--------- src/typings.ts | 7 +++++-- tests/mocks/data.ts | 1 - 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2ced25a..4f9ac43 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ The community-maintained Node.js library for Top.gg. - [Getting a bot](#getting-a-bot) - [Getting several bots](#getting-several-bots) - [Getting your project's voters](#getting-your-projects-voters) - - [Check if a user has voted for your project](#check-if-a-user-has-voted-for-your-project) + - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - [Getting your bot's server count](#getting-your-bots-server-count) - [Posting your bot's server count](#posting-your-bots-server-count) - - [Automatically posting your bot's server count every few minutes]#automatically-posting-your-bots-server-count-every-few-minutes) + - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) + - [Automatically posting your bot's server count every few minutes](#automatically-posting-your-bots-server-count-every-few-minutes) - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) - [Generating widget URLs](#generating-widget-urls) - [Webhooks](#webhooks) @@ -81,8 +82,16 @@ const voters = await client.getVoters(2); ### Getting your project's vote information of a user +#### Discord ID + ```js -const vote = await client.getVote("8226924471638491136"); +const vote = await client.getVote("661200758510977084"); +``` + +#### Top.gg ID + +```js +const vote = await client.getVote("8226924471638491136", "topgg"); ``` ### Getting your bot's server count @@ -99,22 +108,43 @@ await client.postBotServerCount(bot.getServerCount()); ### Posting your bot's application commands list +#### Discord.js + ```js -// Discord.js: const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); -// Eris: +await client.postBotCommands(commands); +``` + +#### Eris + +```js const commands = await bot.getCommands(); -// Discordeno: +await client.postBotCommands(commands); +``` + +#### Discordeno + +```js import { getApplicationCommands } from "discordeno"; const commands = await getApplicationCommands(bot); -// Harmony: +await client.postBotCommands(commands); +``` + +#### Harmony + +```js const commands = await bot.interactions.commands.all(); -// Oceanic: +await client.postBotCommands(commands); +``` + +#### Oceanic + +```js const commands = await bot.application.getGlobalCommands(); await client.postBotCommands(commands); diff --git a/package.json b/package.json index 6ceabb1..8d1ded0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@top-gg/sdk", - "version": "3.1.6", + "version": "3.2.0", "description": "Official Top.gg Node SDK", "main": "./dist/index.js", "scripts": { diff --git a/src/structs/Api.ts b/src/structs/Api.ts index b14f7c2..e570c2a 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -13,10 +13,11 @@ import { ShortUser, BotsQuery, Vote, + UserSource, } from "../typings"; /** - * Top.gg API Client for Posting stats or Fetching data + * Top.gg API Client * * @example * ```js @@ -87,8 +88,8 @@ export class Api extends EventEmitter { let responseBody: string | object | undefined; if ( - (response.headers["content-type"] as string)?.startsWith( - "application/json" + (response.headers["content-type"] as string)?.includes( + "json" ) ) { responseBody = await response.body.json() as object; @@ -222,7 +223,7 @@ export class Api extends EventEmitter { * const voters2 = await client.getVoters(2); * ``` * - * @param {number} [page] The page number. Each page can only have at most 100 voters. + * @param {number} [page] The page number. Page numbers start at 1. Each page can only have at most 100 voters. * @returns {ShortUser[]} Array of 100 unique voters */ public async getVoters(page?: number): Promise { @@ -257,21 +258,28 @@ export class Api extends EventEmitter { * * @example * ```js - * const vote = await client.getVote("8226924471638491136"); + * // Discord ID + * const vote = await client.getVote("661200758510977084"); + * + * // Top.gg ID + * const vote = await client.getVote("8226924471638491136", "topgg"); * ``` * - * @param {Snowflake} id The Top.gg user's ID. - * @returns {?Vote} The user's latest vote information on your project or null if the user has not voted for your project in the past 12 hours. + * @param {Snowflake} id The user's ID. + * @param {UserSource} source The ID type to use. Defaults to "discord". + * + * @returns {Vote | null} The user's latest vote information on your project or null if the user has not voted for your project in the past 12 hours. */ - public async getVote(id: Snowflake): Promise { + public async getVote(id: Snowflake, source: UserSource = "discord"): Promise { if (!id) throw new Error("Missing ID"); + if (!source) source = "discord"; if (this.legacy) { throw new Error("This endpoint is inaccessible with legacy API tokens."); } try { - const response = await this._request("GET", `/v1/projects/@me/votes/${id}`); + const response = await this._request("GET", `/v1/projects/@me/votes/${id}?source=${source}`); return { votedAt: response.created_at, diff --git a/src/typings.ts b/src/typings.ts index 7dc09bf..4771c81 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -9,6 +9,9 @@ export interface APIOptions { /** Discord ID */ export type Snowflake = string; +/** A user account from an external platform that is linked to a Top.gg user account. */ +export type UserSource = "discord" | "topgg"; + export interface BotInfo { /** The Top.gg ID of the bot */ id: Snowflake; @@ -80,9 +83,9 @@ export interface BotsResponse { } export interface Vote { - /** When this vote was cast */ + /** When the vote was cast */ votedAt?: string; - /** When this vote expires and the user is required to vote again */ + /** When the vote expires and the user is required to vote again */ expiresAt?: string; /** This vote's weight */ weight?: number; diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index c158848..9f68ed0 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -38,7 +38,6 @@ export const RAW_VOTE = { }; export const VOTE = { - voted: true, votedAt: "2025-09-09T08:55:16.218761+00:00", expiresAt: "2025-09-09T20:55:16.218761+00:00", weight: 1 From 157a44c971ed6d85d8e7218f86cec2ce84b0c4c7 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 Sep 2025 10:01:55 +0700 Subject: [PATCH 12/48] [doc,revert]: rename WebhookVotePayload to WebhookPayload, TopggAPIError to APIError, and describe weight property --- src/structs/Api.ts | 6 +++--- src/structs/Webhook.ts | 8 ++++---- src/typings.ts | 4 ++-- src/utils/ApiError.ts | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index e570c2a..e36e9ec 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -1,7 +1,7 @@ import type { APIApplicationCommand } from "discord-api-types/v10"; import type { IncomingHttpHeaders } from "undici/types/header"; import { request, type Dispatcher } from "undici"; -import TopGGAPIError from "../utils/ApiError"; +import APIError from "../utils/ApiError"; import { EventEmitter } from "events"; import { STATUS_CODES } from "http"; @@ -98,7 +98,7 @@ export class Api extends EventEmitter { } if (response.statusCode < 200 || response.statusCode > 299) { - throw new TopGGAPIError( + throw new APIError( response.statusCode, STATUS_CODES[response.statusCode] ?? "", responseBody @@ -287,7 +287,7 @@ export class Api extends EventEmitter { weight: response.weight }; } catch (err) { - const topggError = err as TopGGAPIError; + const topggError = err as APIError; if ((topggError?.body as { title?: string })?.title === "Vote expired") { return null; diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index f39656d..ff5d287 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -1,6 +1,6 @@ import getBody from "raw-body"; import { Request, Response, NextFunction } from "express"; -import { WebhookVotePayload } from "../typings"; +import { WebhookPayload } from "../typings"; export interface WebhookOptions { /** @@ -49,7 +49,7 @@ export class Webhook { }; } - private _formatVotePayload(body: any): WebhookVotePayload { + private _formatVotePayload(body: any): WebhookPayload { return { receiverId: (body.bot ?? body.guild)!, voterId: body.user, @@ -134,13 +134,13 @@ export class Webhook { * })); * ``` * - * @param {(payload: WebhookVotePayload, req?: Request, res?: Response, next?: NextFunction) => void | Promise} fn Vote handling function, this function can also throw an error to + * @param {(payload: WebhookPayload, req?: Request, res?: Response, next?: NextFunction) => void | Promise} fn Vote handling function, this function can also throw an error to * allow for the webhook to resend from Top.gg * @returns An express request handler */ public voteListener( fn: ( - payload: WebhookVotePayload, + payload: WebhookPayload, req?: Request, res?: Response, next?: NextFunction diff --git a/src/typings.ts b/src/typings.ts index 4771c81..6f54cd5 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -87,7 +87,7 @@ export interface Vote { votedAt?: string; /** When the vote expires and the user is required to vote again */ expiresAt?: string; - /** This vote's weight */ + /** This vote's weight. 1 during weekdays, 2 during weekends. */ weight?: number; } @@ -100,7 +100,7 @@ export interface ShortUser { avatar: string; } -export interface WebhookVotePayload { +export interface WebhookPayload { /** The ID of the project that received a vote. */ receiverId: Snowflake; /** The ID of the Top.gg user who voted. */ diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index 5a8bd1c..8cca887 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -4,7 +4,7 @@ const tips = { }; /** API Error */ -export default class TopGGAPIError extends Error { +export default class APIError extends Error { /** Response status code */ public statusCode: number; From 4192bd3e08a8b9ee8620c8c2b704077d55dd50db Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 Sep 2025 10:11:40 +0700 Subject: [PATCH 13/48] refactor: remove redundant checks --- src/structs/Api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index e36e9ec..0a02cba 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -272,7 +272,6 @@ export class Api extends EventEmitter { */ public async getVote(id: Snowflake, source: UserSource = "discord"): Promise { if (!id) throw new Error("Missing ID"); - if (!source) source = "discord"; if (this.legacy) { throw new Error("This endpoint is inaccessible with legacy API tokens."); @@ -289,7 +288,7 @@ export class Api extends EventEmitter { } catch (err) { const topggError = err as APIError; - if ((topggError?.body as { title?: string })?.title === "Vote expired") { + if (topggError.statusCode === 404) { return null; } From 8f986e1e7fbcb108c65305413c633658e9b8c7f3 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 13 Sep 2025 11:57:28 +0700 Subject: [PATCH 14/48] feat: separate v1 from v0 --- src/structs/Api.ts | 139 ++++++++++++++++++++++++-------------------- tests/Api.test.ts | 26 ++------- tests/V1Api.test.ts | 29 +++++++++ 3 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 tests/V1Api.test.ts diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 0a02cba..7e9ac25 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -17,7 +17,7 @@ import { } from "../typings"; /** - * Top.gg API Client + * Top.gg v0 API Client * * @example * ```js @@ -30,8 +30,7 @@ import { * @link {@link https://docs.top.gg | API Reference} */ export class Api extends EventEmitter { - private options: APIOptions; - private legacy: boolean; + protected options: APIOptions; /** * Create Top.gg API instance @@ -53,7 +52,6 @@ export class Api extends EventEmitter { const tokenId = tokenData.id; options.id ??= tokenId; - this.legacy = !("_t" in tokenData); } catch { throw new Error( "Invalid API token state, this should not happen! Please report!" @@ -66,13 +64,13 @@ export class Api extends EventEmitter { }; } - private async _request( + protected async _request( method: Dispatcher.HttpMethod, path: string, body?: Record ): Promise { const headers: IncomingHttpHeaders = {}; - if (this.options.token) headers["authorization"] = `Bearer ${this.options.token}`; + if (this.options.token) headers["authorization"] = this.options.token; if (method !== "GET") headers["content-type"] = "application/json"; let url = `https://top.gg/api${path}`; @@ -108,41 +106,6 @@ export class Api extends EventEmitter { return responseBody; } - /** - * Updates the application commands list in your Discord bot's Top.gg page. - * - * @example - * ```js - * // Discord.js: - * const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); - * - * // Eris: - * const commands = await bot.getCommands(); - * - * // Discordeno: - * import { getApplicationCommands } from "discordeno"; - * - * const commands = await getApplicationCommands(bot); - * - * // Harmony: - * const commands = await bot.interactions.commands.all(); - * - * // Oceanic: - * const commands = await bot.application.getGlobalCommands(); - * - * await client.postBotCommands(commands); - * ``` - * - * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON dicts. This cannot be empty. - */ - public async postBotCommands(commands: APIApplicationCommand[]): Promise { - if (this.legacy) { - throw new Error("This endpoint is inaccessible with legacy API tokens."); - } - - await this._request("POST", "/v1/projects/@me/commands", commands); - } - /** * Post your Discord bot's server count to Top.gg * @@ -231,8 +194,6 @@ export class Api extends EventEmitter { } /** - * @deprecated Use a v1 API token with `getVote()` instead. - * * Get whether or not a user has voted in the last 12 hours * * @example @@ -246,13 +207,81 @@ export class Api extends EventEmitter { public async hasVoted(id: Snowflake): Promise { if (!id) throw new Error("Missing ID"); - console.warn("`hasVoted()` is deprecated. Use a v1 API token with `getVote()` instead."); - return this._request("GET", "/bots/check", { userId: id }).then( (x) => !!x.voted ); } + /** + * Whether or not the weekend multiplier is active + * + * @example + * ```js + * const isWeekend = await client.isWeekend(); + * ``` + * + * @returns {boolean} Whether the multiplier is active + */ + public async isWeekend(): Promise { + return this._request("GET", "/weekend").then((x) => x.is_weekend); + } +} + +/** + * Top.gg v1 API Client + * + * @example + * ```js + * const Topgg = require("@top-gg/sdk"); + * + * const client = new Topgg.V1Api(process.env.TOPGG_TOKEN); + * ``` + * + * @link {@link https://topgg.js.org | Library docs} + * @link {@link https://docs.top.gg | API Reference} + */ +export class V1Api extends Api { + /** + * Create Top.gg API instance + * + * @param {string} token Token or options + * @param {APIOptions} [options] API Options + */ + constructor(token: string, options: APIOptions = {}) { + super(`Bearer ${token}`, options); + } + + /** + * Updates the application commands list in your Discord bot's Top.gg page. + * + * @example + * ```js + * // Discord.js: + * const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); + * + * // Eris: + * const commands = await bot.getCommands(); + * + * // Discordeno: + * import { getApplicationCommands } from "discordeno"; + * + * const commands = await getApplicationCommands(bot); + * + * // Harmony: + * const commands = await bot.interactions.commands.all(); + * + * // Oceanic: + * const commands = await bot.application.getGlobalCommands(); + * + * await client.postBotCommands(commands); + * ``` + * + * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON dicts. This cannot be empty. + */ + public async postBotCommands(commands: APIApplicationCommand[]): Promise { + await this._request("POST", "/v1/projects/@me/commands", commands); + } + /** * Get the latest vote information of a Top.gg user on your project. * @@ -273,10 +302,6 @@ export class Api extends EventEmitter { public async getVote(id: Snowflake, source: UserSource = "discord"): Promise { if (!id) throw new Error("Missing ID"); - if (this.legacy) { - throw new Error("This endpoint is inaccessible with legacy API tokens."); - } - try { const response = await this._request("GET", `/v1/projects/@me/votes/${id}?source=${source}`); @@ -295,18 +320,4 @@ export class Api extends EventEmitter { throw err; } } - - /** - * Whether or not the weekend multiplier is active - * - * @example - * ```js - * const isWeekend = await client.isWeekend(); - * ``` - * - * @returns {boolean} Whether the multiplier is active - */ - public async isWeekend(): Promise { - return this._request("GET", "/weekend").then((x) => x.is_weekend); - } -} +} \ No newline at end of file diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 6f9968d..fccdd98 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,24 +1,10 @@ import { Api } from '../src/index'; import ApiError from '../src/utils/ApiError'; -import { BOT, BOT_STATS, VOTE, VOTES } from './mocks/data'; +import { BOT, BOT_STATS, VOTES } from './mocks/data'; /* mock token */ const client = new Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); -describe('API postBotCommands test', () => { - it('postBotCommands should work', () => { - expect(client.postBotCommands([{ - id: '1', - type: 1, - application_id: '1', - name: 'test', - description: 'command description', - default_member_permissions: '', - version: '1' - }])).resolves.toBeUndefined(); - }); -}); - describe('API postBotServerCount test', () => { it('postBotServerCount with invalid negative server count should throw error', () => { expect(client.postBotServerCount(-1)).rejects.toThrow(Error); @@ -55,13 +41,13 @@ describe('API getVoters test', () => { }); }); -describe('API getVote test', () => { - it('getVote should return 200 when token is provided', () => { - expect(client.getVote('1')).resolves.toStrictEqual(VOTE); +describe('API hasVoted test', () => { + it('hasVoted should return 200 when token is provided', () => { + expect(client.hasVoted('1')).resolves.toBe(true); }); - it('getVote should throw error when no id is provided', () => { - expect(client.getVote('')).rejects.toThrow(Error); + it('hasVoted should throw error when no id is provided', () => { + expect(client.hasVoted('')).rejects.toThrow(Error); }); }); diff --git a/tests/V1Api.test.ts b/tests/V1Api.test.ts new file mode 100644 index 0000000..da6a3a8 --- /dev/null +++ b/tests/V1Api.test.ts @@ -0,0 +1,29 @@ +import { V1Api } from '../src/index'; +import { VOTE } from './mocks/data'; + +/* mock token */ +const client = new V1Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); + +describe('API postBotCommands test', () => { + it('postBotCommands should work', () => { + expect(client.postBotCommands([{ + id: '1', + type: 1, + application_id: '1', + name: 'test', + description: 'command description', + default_member_permissions: '', + version: '1' + }])).resolves.toBeUndefined(); + }); +}); + +describe('API getVote test', () => { + it('getVote should return 200 when token is provided', () => { + expect(client.getVote('1')).resolves.toStrictEqual(VOTE); + }); + + it('getVote should throw error when no id is provided', () => { + expect(client.getVote('')).rejects.toThrow(Error); + }); +}); From 6d866f5306b5c9fb7e9cd61805ca77dc9577d858 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 15 Sep 2025 23:10:16 +0700 Subject: [PATCH 15/48] refactor: use Bearer prefix for legacy and new tokens --- src/structs/Api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 7e9ac25..79eff21 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -70,7 +70,7 @@ export class Api extends EventEmitter { body?: Record ): Promise { const headers: IncomingHttpHeaders = {}; - if (this.options.token) headers["authorization"] = this.options.token; + if (this.options.token) headers["authorization"] = `Bearer ${this.options.token}`; if (method !== "GET") headers["content-type"] = "application/json"; let url = `https://top.gg/api${path}`; @@ -248,7 +248,7 @@ export class V1Api extends Api { * @param {APIOptions} [options] API Options */ constructor(token: string, options: APIOptions = {}) { - super(`Bearer ${token}`, options); + super(token, options); } /** @@ -320,4 +320,4 @@ export class V1Api extends Api { throw err; } } -} \ No newline at end of file +} From 66adfe36e15b86b8a44dc0f5c5b05701a52c4c73 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 15 Sep 2025 23:12:49 +0700 Subject: [PATCH 16/48] doc: add @see Webhook#voteListener --- src/structs/Webhook.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index ff5d287..bcf4ca6 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -6,6 +6,7 @@ export interface WebhookOptions { /** * Handles an error created by the function passed to webhook listeners * + * @see Webhook#voteListener * @default console.error */ error?: (error: Error) => void | Promise; From 3a234adc43f0dd0d9562c0def6a7fb8c7f0531a7 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 15 Sep 2025 23:47:06 +0700 Subject: [PATCH 17/48] doc: update documentation and readme --- README.md | 174 ++++++++++++++++++++------------------------- src/structs/Api.ts | 2 +- 2 files changed, 78 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 4f9ac43..9c74ba3 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,19 @@ The community-maintained Node.js library for Top.gg. - [Installation](#installation) - [Setting up](#setting-up) - [Usage](#usage) - - [Getting a bot](#getting-a-bot) - - [Getting several bots](#getting-several-bots) - - [Getting your project's voters](#getting-your-projects-voters) - - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - - [Getting your bot's server count](#getting-your-bots-server-count) - - [Posting your bot's server count](#posting-your-bots-server-count) - - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) - - [Automatically posting your bot's server count every few minutes](#automatically-posting-your-bots-server-count-every-few-minutes) - - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) - - [Generating widget URLs](#generating-widget-urls) + - [API v1](#api-v1) + - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) + - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) + - [API v0](#api-v0) + - [Getting a bot](#getting-a-bot) + - [Getting several bots](#getting-several-bots) + - [Getting your project's voters](#getting-your-projects-voters) + - [Check if a user has voted for your project](#check-if-a-user-has-voted-for-your-project) + - [Getting your bot's server count](#getting-your-bots-server-count) + - [Posting your bot's server count](#posting-your-bots-server-count) + - [Automatically posting your bot's server count every few minutes](#automatically-posting-your-bots-server-count-every-few-minutes) + - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) + - [Generating widget URLs](#generating-widget-urls) - [Webhooks](#webhooks) - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) @@ -36,15 +39,15 @@ $ yarn add @top-gg/sdk ## Setting up -### CommonJS +### v1 ```js -const Topgg = require("@top-gg/sdk"); +import Topgg from "@top-gg/sdk"; -const client = new Topgg.Api(process.env.TOPGG_TOKEN); +const client = new Topgg.V1Api(process.env.TOPGG_TOKEN); ``` -### ES module +### v0 ```js import Topgg from "@top-gg/sdk"; @@ -54,113 +57,123 @@ const client = new Topgg.Api(process.env.TOPGG_TOKEN); ## Usage -### Getting a bot +### API v1 + +#### Getting your project's vote information of a user + +##### Discord ID ```js -const bot = await client.getBot("461521980492087297"); +const vote = await client.getVote("661200758510977084"); ``` -### Getting several bots +##### Top.gg ID ```js -const bots = await client.getBots(); +const vote = await client.getVote("8226924471638491136", "topgg"); ``` -### Getting your project's voters +#### Posting your bot's application commands list -#### First page +##### Discord.js ```js -const voters = await client.getVoters(); +const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); + +await client.postBotCommands(commands); ``` -#### Subsequent pages +##### Eris ```js -const voters = await client.getVoters(2); -``` +const commands = await bot.getCommands(); -### Getting your project's vote information of a user +await client.postBotCommands(commands); +``` -#### Discord ID +##### Discordeno ```js -const vote = await client.getVote("661200758510977084"); -``` +import { getApplicationCommands } from "discordeno"; -#### Top.gg ID +const commands = await getApplicationCommands(bot); -```js -const vote = await client.getVote("8226924471638491136", "topgg"); +await client.postBotCommands(commands); ``` -### Getting your bot's server count +##### Harmony ```js -const serverCount = await client.getBotServerCount(); +const commands = await bot.interactions.commands.all(); + +await client.postBotCommands(commands); ``` -### Posting your bot's server count +##### Oceanic ```js -await client.postBotServerCount(bot.getServerCount()); +const commands = await bot.application.getGlobalCommands(); + +await client.postBotCommands(commands); ``` -### Posting your bot's application commands list +### API v0 -#### Discord.js +#### Getting a bot ```js -const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); - -await client.postBotCommands(commands); +const bot = await client.getBot("461521980492087297"); ``` -#### Eris +#### Getting several bots ```js -const commands = await bot.getCommands(); - -await client.postBotCommands(commands); +const bots = await client.getBots(); ``` -#### Discordeno +#### Getting your project's voters + +##### First page ```js -import { getApplicationCommands } from "discordeno"; +const voters = await client.getVoters(); +``` -const commands = await getApplicationCommands(bot); +##### Subsequent pages -await client.postBotCommands(commands); +```js +const voters = await client.getVoters(2); ``` -#### Harmony +#### Check if a user has voted for your project ```js -const commands = await bot.interactions.commands.all(); - -await client.postBotCommands(commands); +const hasVoted = await client.hasVoted("661200758510977084"); ``` -#### Oceanic +#### Getting your bot's server count ```js -const commands = await bot.application.getGlobalCommands(); +const serverCount = await client.getBotServerCount(); +``` -await client.postBotCommands(commands); +#### Posting your bot's server count + +```js +await client.postBotServerCount(bot.getServerCount()); ``` -### Automatically posting your bot's server count every few minutes +#### Automatically posting your bot's server count every few minutes You would need to use the third-party `topgg-autoposter` package to be able to autopost. Install it in your terminal like so: -#### NPM +##### NPM ```sh $ npm i topgg-autoposter ``` -#### Yarn +##### Yarn ```sh $ yarn add topgg-autoposter @@ -168,21 +181,6 @@ $ yarn add topgg-autoposter Then in your code: -#### CommonJS - -```js -const { AutoPoster } = require("topgg-autoposter"); - -// Your discord.js client or any other -const client = Discord.Client(); - -AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { - console.log("Successfully posted server count to Top.gg!"); -}); -``` - -#### ES module - ```js import { AutoPoster } from "topgg-autoposter"; @@ -194,33 +192,33 @@ AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { }); ``` -### Checking if the weekend vote multiplier is active +#### Checking if the weekend vote multiplier is active ```js const isWeekend = await client.isWeekend(); ``` -### Generating widget URLs +#### Generating widget URLs -#### Large +##### Large ```js const widgetUrl = Topgg.Widget.large(Topgg.WidgetType.DiscordBot, "574652751745777665"); ``` -#### Votes +##### Votes ```js const widgetUrl = Topgg.Widget.votes(Topgg.WidgetType.DiscordBot, "574652751745777665"); ``` -#### Owner +##### Owner ```js const widgetUrl = Topgg.Widget.owner(Topgg.WidgetType.DiscordBot, "574652751745777665"); ``` -#### Social +##### Social ```js const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745777665"); @@ -232,24 +230,6 @@ const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745 With express: -##### CommonJS - -```js -const { Webhook } = require("@top-gg/sdk"); -const express = require("express"); - -const app = express(); -const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); - -app.post("/votes", webhook.voteListener(vote => { - console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); -})); - -app.listen(8080); -``` - -##### ES module - ```js import { Webhook } from "@top-gg/sdk"; import express from "express"; diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 79eff21..bc1773d 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -198,7 +198,7 @@ export class Api extends EventEmitter { * * @example * ```js - * const hasVoted = await client.hasVoted("205680187394752512"); + * const hasVoted = await client.hasVoted("661200758510977084"); * ``` * * @param {Snowflake} id User ID From b2652b2ddea3dd01512d2a79f9986aab05fbf518 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 15 Sep 2025 23:58:35 +0700 Subject: [PATCH 18/48] meta: make SDK description clearer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d1ded0..f2e0afe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@top-gg/sdk", "version": "3.2.0", - "description": "Official Top.gg Node SDK", + "description": "A community-maintained Node.js API Client for the Top.gg API.", "main": "./dist/index.js", "scripts": { "test": "jest --verbose", From 89be620243db4bc3ef35f3a673b04cdfec91e553 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 16 Sep 2025 10:23:07 +0700 Subject: [PATCH 19/48] revert: revert breaking changes to webhooks --- README.md | 4 +- src/structs/Webhook.ts | 137 +++++++++++++++++++++++------------------ src/typings.ts | 27 ++++---- 3 files changed, 94 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 9c74ba3..149382d 100644 --- a/README.md +++ b/README.md @@ -237,8 +237,8 @@ import express from "express"; const app = express(); const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); -app.post("/votes", webhook.voteListener(vote => { - console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); +app.post("/votes", webhook.listener(vote => { + console.log(`A user with the ID of ${vote.user} has voted us on Top.gg!`); })); app.listen(8080); diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index bcf4ca6..c5ebb1b 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -4,9 +4,8 @@ import { WebhookPayload } from "../typings"; export interface WebhookOptions { /** - * Handles an error created by the function passed to webhook listeners + * Handles an error created by the function passed to Webhook.listener() * - * @see Webhook#voteListener * @default console.error */ error?: (error: Error) => void | Promise; @@ -17,17 +16,22 @@ export interface WebhookOptions { * * @example * ```js - * const { Webhook } = require("@top-gg/sdk"); * const express = require("express"); - * + * const { Webhook } = require("@top-gg/sdk"); + * * const app = express(); - * const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); - * - * app.post("/votes", webhook.voteListener(vote => { - * console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); + * const wh = new Webhook("webhookauth123"); + * + * app.post("/dblwebhook", wh.listener((vote) => { + * // vote is your vote object e.g + * console.log(vote.user); // => 321714991050784770 * })); - * - * app.listen(8080); + * + * app.listen(80); + * + * // In this situation, your TopGG Webhook dashboard should look like + * // URL = http://your.server.ip:80/dblwebhook + * // Authorization: webhookauth123 * ``` * * @link {@link https://docs.top.gg/resources/webhooks/#schema | Webhook Data Schema} @@ -39,43 +43,43 @@ export class Webhook { /** * Create a new webhook client instance * - * @param {?string} authorization Webhook authorization to verify requests + * @param authorization Webhook authorization to verify requests */ - constructor( - private authorization?: string, - options: WebhookOptions = {} - ) { + constructor(private authorization?: string, options: WebhookOptions = {}) { this.options = { - error: options.error ?? console.error + error: options.error ?? console.error, }; } - private _formatVotePayload(body: any): WebhookPayload { - return { - receiverId: (body.bot ?? body.guild)!, - voterId: body.user, - isTest: body.type === "test", - isWeekend: body.isWeekend, - query: body.query ?? Object.fromEntries(new URLSearchParams(body.query)) - }; + private _formatIncoming( + body: WebhookPayload & { query: string } + ): WebhookPayload { + const out: WebhookPayload = { ...body }; + if (body?.query?.length > 0) + out.query = Object.fromEntries(new URLSearchParams(body.query)); + return out; } - private _parseRequest(req: Request, res: Response): Promise { + private _parseRequest( + req: Request, + res: Response + ): Promise { return new Promise((resolve) => { if ( this.authorization && req.headers.authorization !== this.authorization ) return res.status(401).json({ error: "Unauthorized" }); - // parse json - if (req.body) return resolve(req.body); + if (req.body) return resolve(this._formatIncoming(req.body)); getBody(req, {}, (error, body) => { if (error) return res.status(422).json({ error: "Malformed request" }); try { - resolve(JSON.parse(body.toString("utf8"))); + const parsed = JSON.parse(body.toString("utf8")); + + resolve(this._formatIncoming(parsed)); } catch { res.status(400).json({ error: "Invalid body" }); resolve(false); @@ -84,10 +88,32 @@ export class Webhook { }); } - private _listener( - formatFn: (data: any) => T, - callbackFn: ( - payload: T, + /** + * Listening function for handling webhook requests + * + * @example + * ```js + * app.post("/webhook", wh.listener((vote) => { + * console.log(vote.user); // => 395526710101278721 + * })); + * ``` + * + * @example + * ```js + * // Throwing an error to resend the webhook + * app.post("/webhook/", wh.listener((vote) => { + * // for example, if your bot is offline, you should probably not handle votes and try again + * if (bot.offline) throw new Error('Bot offline'); + * })); + * ``` + * + * @param fn Vote handling function, this function can also throw an error to + * allow for the webhook to resend from Top.gg + * @returns An express request handler + */ + public listener( + fn: ( + payload: WebhookPayload, req?: Request, res?: Response, next?: NextFunction @@ -99,11 +125,10 @@ export class Webhook { next: NextFunction ): Promise => { const response = await this._parseRequest(req, res); - if (!response) return; try { - await callbackFn(formatFn(response), req, res, next); + await fn(response, req, res, next); if (!res.headersSent) { res.sendStatus(204); @@ -117,36 +142,28 @@ export class Webhook { } /** - * Listening function for handling webhook requests + * Middleware function to pass to express, sets req.vote to the payload * + * @deprecated Use the new {@link Webhook.listener | .listener()} function * @example * ```js - * app.post("/votes", webhook.voteListener(vote => { - * console.log(`A user with the ID of ${vote.voterId} has voted us on Top.gg!`); - * })); + * app.post("/dblwebhook", wh.middleware(), (req, res) => { + * // req.vote is your payload e.g + * console.log(req.vote.user); // => 395526710101278721 + * }); * ``` - * - * @example - * ```js - * // Throwing an error to resend the webhook - * app.post("/votes", webhook.voteListener(vote => { - * // For example, if your bot is offline, you should probably not handle votes and try again. - * if (bot.offline) throw new Error('Bot offline'); - * })); - * ``` - * - * @param {(payload: WebhookPayload, req?: Request, res?: Response, next?: NextFunction) => void | Promise} fn Vote handling function, this function can also throw an error to - * allow for the webhook to resend from Top.gg - * @returns An express request handler */ - public voteListener( - fn: ( - payload: WebhookPayload, - req?: Request, - res?: Response, - next?: NextFunction - ) => void | Promise - ) { - return this._listener(this._formatVotePayload, fn); + public middleware() { + return async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + const response = await this._parseRequest(req, res); + if (!response) return; + res.sendStatus(204); + req.vote = response; + next(); + }; } } diff --git a/src/typings.ts b/src/typings.ts index 6f54cd5..278d0fc 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -101,18 +101,15 @@ export interface ShortUser { } export interface WebhookPayload { - /** The ID of the project that received a vote. */ - receiverId: Snowflake; - /** The ID of the Top.gg user who voted. */ - voterId: Snowflake; - /** - * Whether this vote is just a test done from the page settings. - */ - isTest: boolean; - /** - * Whether the weekend multiplier is in effect, meaning users votes count as - * two - */ + /** If webhook is a Discord bot: ID of the bot that received a vote */ + bot?: Snowflake; + /** If webhook is a server: ID of the server that received a vote */ + guild?: Snowflake; + /** ID of the user who voted */ + user: Snowflake; + /** The type of the vote (should always be "upvote" except when using the test button it's "test") */ + type: string; + /** Whether the weekend multiplier is in effect, meaning users votes count as two */ isWeekend?: boolean; /** Query parameters in vote page in a key to value object */ query: @@ -121,3 +118,9 @@ export interface WebhookPayload { } | string; } + +declare module "express" { + export interface Request { + vote?: WebhookPayload; + } +} From 6fe8cf46176f625141376b2c7553c6348966cb98 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 18 Sep 2025 22:41:18 +0700 Subject: [PATCH 20/48] revert: revert breaking changes --- src/structs/Api.ts | 70 +++++++++++++++++++---- src/typings.ts | 135 +++++++++++++++++++++++++++++++++++++++++++-- tests/Api.test.ts | 34 ++++++++---- 3 files changed, 211 insertions(+), 28 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index bc1773d..c53ff7f 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -10,9 +10,11 @@ import { Snowflake, BotInfo, BotsResponse, + BotStats, ShortUser, BotsQuery, Vote, + UserInfo, UserSource, } from "../typings"; @@ -107,37 +109,58 @@ export class Api extends EventEmitter { } /** - * Post your Discord bot's server count to Top.gg + * Post your bot's stats to Top.gg * * @example * ```js - * await client.postBotServerCount(bot.getServerCount()); + * await api.postStats({ + * serverCount: 28199, + * }); * ``` * - * @param {number} serverCount Server count + * @param {object} stats Stats object + * @param {number} stats.serverCount Server count + * @returns {BotStats} Passed object */ - public async postBotServerCount(serverCount: number): Promise { - if ((serverCount ?? 0) <= 0) throw new Error("Missing server count"); + public async postStats(stats: BotStats): Promise { + if ((stats?.serverCount ?? 0) <= 0) throw new Error("Missing server count"); /* eslint-disable camelcase */ await this._request("POST", "/bots/stats", { - server_count: serverCount, + server_count: stats.serverCount, }); /* eslint-enable camelcase */ + + return stats; } /** - * Get your Discord bot's server count + * Get your bot's stats * * @example * ```js - * const serverCount = await client.getBotServerCount(); + * await api.getStats(); + * // => + * { + * serverCount: 28199, + * shardCount: null, + * shards: [] + * } * ``` * - * @returns {number} Your bot's server count + * @returns {BotStats} Your bot's stats */ - public async getBotServerCount(): Promise { - return (await this._request("GET", "/bots/stats")).server_count; + public async getStats(_id?: Snowflake): Promise { + if (_id) + console.warn( + "[DeprecationWarning] getStats() no longer needs an ID argument" + ); + const result = await this._request("GET", "/bots/stats"); + return { + serverCount: result.server_count, + shardCount: null, + shards: [], + }; } /** @@ -156,6 +179,29 @@ export class Api extends EventEmitter { return this._request("GET", `/bots/${id}`); } + /** + * @deprecated No longer supported by Top.gg API v0. + * + * Get user info + * + * @example + * ```js + * await api.getUser("205680187394752512"); + * // => + * user.username; // Xignotic + * ``` + * + * @param {Snowflake} _id User ID + * @returns {UserInfo} Info for user + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async getUser(_id: Snowflake): Promise { + throw new APIError( + 404, + STATUS_CODES[404]!, + "getUser is no longer supported by Top.gg API v0." + ); + } + /** * Get a list of bots * @@ -189,7 +235,7 @@ export class Api extends EventEmitter { * @param {number} [page] The page number. Page numbers start at 1. Each page can only have at most 100 voters. * @returns {ShortUser[]} Array of 100 unique voters */ - public async getVoters(page?: number): Promise { + public async getVotes(page?: number): Promise { return this._request("GET", `/bots/${this.options.id}/votes`, { page: page ?? 1 }); } diff --git a/src/typings.ts b/src/typings.ts index 278d0fc..a0b17c0 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -1,14 +1,14 @@ +/** Discord ID */ +export type Snowflake = string; + export interface APIOptions { /** Top.gg API token */ token?: string; /** Client ID to use */ - id?: string; + id?: Snowflake; } -/** Discord ID */ -export type Snowflake = string; - /** A user account from an external platform that is linked to a Top.gg user account. */ export type UserSource = "discord" | "topgg"; @@ -19,8 +19,32 @@ export interface BotInfo { clientid: Snowflake; /** The username of the bot */ username: string; + /** + * The discriminator of the bot + * + * @deprecated No longer supported by Top.gg API v0. + */ + discriminator: string; /** The bot's avatar */ avatar: string; + /** + * The cdn hash of the bot's avatar if the bot has none + * + * @deprecated No longer supported by Top.gg API v0. + */ + defAvatar: string; + /** + * The URL for the banner image + * + * @deprecated No longer supported by Top.gg API v0. + */ + bannerUrl?: string; + /** + * The library of the bot + * + * @deprecated No longer supported by Top.gg API v0. + */ + lib: string; /** The prefix of the bot */ prefix: string; /** The short description of the bot */ @@ -37,16 +61,34 @@ export interface BotInfo { github?: string; /** The owners of the bot. First one in the array is the main owner */ owners: Snowflake[]; + /** + * The guilds featured on the bot page + * + * @deprecated No longer supported by Top.gg API v0. + */ + guilds: Snowflake[]; /** The custom bot invite url of the bot */ invite?: string; /** The date when the bot was submitted (in ISO 8601) */ date: string; + /** + * The certified status of the bot + * + * @deprecated No longer supported by Top.gg API v0. + */ + certifiedBot: boolean; /** The vanity url of the bot */ vanity?: string; /** The amount of votes the bot has */ points: number; /** The amount of votes the bot has this month */ monthlyPoints: number; + /** + * The guild id for the donatebot setup + * + * @deprecated No longer supported by Top.gg API v0. + */ + donatebotguildid: Snowflake; /** The amount of servers the bot is in based on posted stats */ server_count?: number; /** The bot's reviews on Top.gg */ @@ -58,11 +100,90 @@ export interface BotInfo { }; } +export interface BotStats { + /** The amount of servers the bot is in */ + serverCount?: number; + /** + * The amount of servers the bot is in per shard. Always present but can be + * empty. (Only when receiving stats) + * + * @deprecated No longer supported by Top.gg API v0. + */ + shards?: number[]; + /** + * The shard ID to post as (only when posting) + * + * @deprecated No longer supported by Top.gg API v0. + */ + shardId?: number; + /** + * The amount of shards a bot has + * + * @deprecated No longer supported by Top.gg API v0. + */ + shardCount?: number | null; +} + +/** + * @deprecated No longer supported by Top.gg API v0. + */ +export interface UserInfo { + /** The id of the user */ + id: Snowflake; + /** The username of the user */ + username: string; + /** The discriminator of the user */ + discriminator: string; + /** The user's avatar url */ + avatar: string; + /** The cdn hash of the user's avatar if the user has none */ + defAvatar: string; + /** The bio of the user */ + bio?: string; + /** The banner image url of the user */ + banner?: string; + /** The social usernames of the user */ + social: { + /** The youtube channel id of the user */ + youtube?: string; + /** The reddit username of the user */ + reddit?: string; + /** The twitter username of the user */ + twitter?: string; + /** The instagram username of the user */ + instagram?: string; + /** The github username of the user */ + github?: string; + }; + /** The custom hex color of the user */ + color: string; + /** The supporter status of the user */ + supporter: boolean; + /** The certified status of the user */ + certifiedDev: boolean; + /** The mod status of the user */ + mod: boolean; + /** The website moderator status of the user */ + webMod: boolean; + /** The admin status of the user */ + admin: boolean; +} + export interface BotsQuery { /** The amount of bots to return. Max. 500 */ limit?: number; /** Amount of bots to skip */ offset?: number; + /** + * A search string in the format of "field: value field2: value2" + * + * @deprecated No longer supported by Top.gg API v0. + */ + search?: + | { + [key in keyof BotInfo]: string; + } + | string; /** Sorts results from a specific criteria. Results will always be descending. */ sort?: "monthlyPoints" | "id" | "date"; /** A list of fields to show. */ @@ -96,6 +217,12 @@ export interface ShortUser { id: Snowflake; /** User's username */ username: string; + /** + * User's discriminator + * + * @deprecated No longer supported by Top.gg API v0. + */ + discriminator: string; /** User's avatar url */ avatar: string; } diff --git a/tests/Api.test.ts b/tests/Api.test.ts index fccdd98..89259e7 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -5,19 +5,29 @@ import { BOT, BOT_STATS, VOTES } from './mocks/data'; /* mock token */ const client = new Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); -describe('API postBotServerCount test', () => { - it('postBotServerCount with invalid negative server count should throw error', () => { - expect(client.postBotServerCount(-1)).rejects.toThrow(Error); +describe('API postStats test', () => { + it('postStats without server count should throw error', async () => { + await expect(client.postStats({ shardCount: 0 })).rejects.toThrow(Error); }); - it('postBotServerCount should return 200', async () => { - await expect(client.postBotServerCount(1)).resolves.toBeUndefined(); + it('postStats with invalid negative server count should throw error', () => { + expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); + }); + + it('postStats should return 200', async () => { + await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( + Object + ); }); }); -describe('API getBotServerCount test', () => { - it('getBotServerCount should return 200 when bot is found', async () => { - expect(client.getBotServerCount()).resolves.toStrictEqual(BOT_STATS.server_count); +describe('API getStats test', () => { + it('getStats should return 200 when bot is found', async () => { + expect(client.getStats()).resolves.toStrictEqual({ + serverCount: BOT_STATS.server_count, + shardCount: BOT_STATS.shard_count, + shards: BOT_STATS.shards + }); }); }); @@ -35,9 +45,9 @@ describe('API getBot test', () => { }); }); -describe('API getVoters test', () => { - it('getVoters should return 200 when token is provided', () => { - expect(client.getVoters()).resolves.toStrictEqual(VOTES); +describe('API getVotes test', () => { + it('getVotes should return 200 when token is provided', () => { + expect(client.getVotes()).resolves.toEqual(VOTES); }); }); @@ -55,4 +65,4 @@ describe('API isWeekend tests', () => { it('isWeekend should return true', async () => { expect(client.isWeekend()).resolves.toBe(true); }); -}); +}); \ No newline at end of file From 840abb8dba842d25e67eae2f213f547cce8413e0 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 3 Oct 2025 23:33:06 +0700 Subject: [PATCH 21/48] doc: update readme --- README.md | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 149382d..95c4922 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ # Top.gg Node.js SDK +> For more information, see the documentation here: https://topgg.js.org. + The community-maintained Node.js library for Top.gg. ## Chapters - [Installation](#installation) + - [NPM](#npm) + - [Yarn](#yarn) - [Setting up](#setting-up) + - [v1](#v1) + - [v0](#v0) - [Usage](#usage) - [API v1](#api-v1) - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) @@ -15,16 +21,18 @@ The community-maintained Node.js library for Top.gg. - [Getting several bots](#getting-several-bots) - [Getting your project's voters](#getting-your-projects-voters) - [Check if a user has voted for your project](#check-if-a-user-has-voted-for-your-project) - - [Getting your bot's server count](#getting-your-bots-server-count) - - [Posting your bot's server count](#posting-your-bots-server-count) - - [Automatically posting your bot's server count every few minutes](#automatically-posting-your-bots-server-count-every-few-minutes) + - [Getting your bot's statistics](#getting-your-bots-statistics) + - [Posting your bot's statistics](#posting-your-bots-statistics) + - [Automatically posting your bot's statistics every few minutes](#automatically-posting-your-bots-statistics-every-few-minutes) - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) - [Generating widget URLs](#generating-widget-urls) - [Webhooks](#webhooks) - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) + ## Installation + ### NPM ```sh @@ -39,6 +47,7 @@ $ yarn add @top-gg/sdk ## Setting up + ### v1 ```js @@ -61,6 +70,7 @@ const client = new Topgg.Api(process.env.TOPGG_TOKEN); #### Getting your project's vote information of a user + ##### Discord ID ```js @@ -75,6 +85,7 @@ const vote = await client.getVote("8226924471638491136", "topgg"); #### Posting your bot's application commands list + ##### Discord.js ```js @@ -133,6 +144,7 @@ const bots = await client.getBots(); #### Getting your project's voters + ##### First page ```js @@ -151,22 +163,26 @@ const voters = await client.getVoters(2); const hasVoted = await client.hasVoted("661200758510977084"); ``` -#### Getting your bot's server count +#### Getting your bot's statistics ```js -const serverCount = await client.getBotServerCount(); +const stats = await client.getStats(); ``` -#### Posting your bot's server count +#### Posting your bot's statistics ```js -await client.postBotServerCount(bot.getServerCount()); + +await api.postStats({ + serverCount: bot.getServerCount(), +}); ``` -#### Automatically posting your bot's server count every few minutes +#### Automatically posting your bot's statistics every few minutes You would need to use the third-party `topgg-autoposter` package to be able to autopost. Install it in your terminal like so: + ##### NPM ```sh @@ -188,7 +204,7 @@ import { AutoPoster } from "topgg-autoposter"; const client = Discord.Client(); AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { - console.log("Successfully posted server count to Top.gg!"); + console.log("Successfully posted statistics to Top.gg!"); }); ``` @@ -200,6 +216,7 @@ const isWeekend = await client.isWeekend(); #### Generating widget URLs + ##### Large ```js @@ -235,7 +252,7 @@ import { Webhook } from "@top-gg/sdk"; import express from "express"; const app = express(); -const webhook = new Webhook(process.env.MY_TOPGG_WEBHOOK_SECRET); +const webhook = new Webhook(process.env.TOPGG_WEBHOOK_PASSWORD); app.post("/votes", webhook.listener(vote => { console.log(`A user with the ID of ${vote.user} has voted us on Top.gg!`); From 15fb73a1a7e3bb37ffc8cd3225d62d51dd10336f Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 7 Oct 2025 12:49:17 +0700 Subject: [PATCH 22/48] ci: bump github workflow dependency versions --- .github/workflows/build.yml | 6 +++--- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6232ab2..0c3e42f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,9 @@ jobs: matrix: node: [18, 20] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Cache node_modules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -26,7 +26,7 @@ jobs: ${{ runner.os }}-build- ${{ runner.os }}- - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} check-latest: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2d6d3e7..d545b16 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,8 +8,8 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 with: node-version: 18 check-latest: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bce1efc..65d32d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version: 18 check-latest: true From 1ddf708c964aceb5f0cbecf070658d958327e5ab Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 10 Oct 2025 11:40:01 +0700 Subject: [PATCH 23/48] doc: fix less accurate naming --- src/structs/Api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index c53ff7f..9373f21 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -19,7 +19,7 @@ import { } from "../typings"; /** - * Top.gg v0 API Client + * Top.gg API v0 Client * * @example * ```js @@ -274,7 +274,7 @@ export class Api extends EventEmitter { } /** - * Top.gg v1 API Client + * Top.gg API v1 Client * * @example * ```js From 59c5257553cdee89f12dda552c2c609320191d67 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 10 Oct 2025 11:54:07 +0700 Subject: [PATCH 24/48] doc: objects, not dicts --- src/structs/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 9373f21..2daa1f1 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -322,7 +322,7 @@ export class V1Api extends Api { * await client.postBotCommands(commands); * ``` * - * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON dicts. This cannot be empty. + * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON objects. This cannot be empty. */ public async postBotCommands(commands: APIApplicationCommand[]): Promise { await this._request("POST", "/v1/projects/@me/commands", commands); From aa408ac638973f2229c3242578cd6a611c065183 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 10 Oct 2025 13:40:37 +0700 Subject: [PATCH 25/48] doc: documentation tweaks --- README.md | 36 +++++++++++++++++++++++------------- src/structs/Api.ts | 21 +++++++++++++++++++-- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 95c4922..76bdd24 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,7 @@ The community-maintained Node.js library for Top.gg. ## Chapters - [Installation](#installation) - - [NPM](#npm) - - [Yarn](#yarn) - [Setting up](#setting-up) - - [v1](#v1) - - [v0](#v0) - [Usage](#usage) - [API v1](#api-v1) - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) @@ -29,10 +25,8 @@ The community-maintained Node.js library for Top.gg. - [Webhooks](#webhooks) - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) - ## Installation - ### NPM ```sh @@ -47,8 +41,9 @@ $ yarn add @top-gg/sdk ## Setting up +### API v1 -### v1 +Note that API v1 also includes API v0. ```js import Topgg from "@top-gg/sdk"; @@ -56,7 +51,7 @@ import Topgg from "@top-gg/sdk"; const client = new Topgg.V1Api(process.env.TOPGG_TOKEN); ``` -### v0 +### API v0 ```js import Topgg from "@top-gg/sdk"; @@ -70,7 +65,6 @@ const client = new Topgg.Api(process.env.TOPGG_TOKEN); #### Getting your project's vote information of a user - ##### Discord ID ```js @@ -85,7 +79,6 @@ const vote = await client.getVote("8226924471638491136", "topgg"); #### Posting your bot's application commands list - ##### Discord.js ```js @@ -128,6 +121,26 @@ const commands = await bot.application.getGlobalCommands(); await client.postBotCommands(commands); ``` +##### Raw + +```js +await client.postBotCommands([ + { + options: [], + name: 'test', + name_localizations: null, + description: 'command description', + description_localizations: null, + contexts: [], + default_permission: null, + default_member_permissions: null, + dm_permission: false, + integration_types: [], + nsfw: false + } +]); +``` + ### API v0 #### Getting a bot @@ -144,7 +157,6 @@ const bots = await client.getBots(); #### Getting your project's voters - ##### First page ```js @@ -182,7 +194,6 @@ await api.postStats({ You would need to use the third-party `topgg-autoposter` package to be able to autopost. Install it in your terminal like so: - ##### NPM ```sh @@ -216,7 +227,6 @@ const isWeekend = await client.isWeekend(); #### Generating widget URLs - ##### Large ```js diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 2daa1f1..d2a95f2 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -19,7 +19,7 @@ import { } from "../typings"; /** - * Top.gg API v0 Client + * Top.gg API v0 client * * @example * ```js @@ -274,7 +274,7 @@ export class Api extends EventEmitter { } /** - * Top.gg API v1 Client + * Top.gg API v1 client * * @example * ```js @@ -320,6 +320,23 @@ export class V1Api extends Api { * const commands = await bot.application.getGlobalCommands(); * * await client.postBotCommands(commands); + * + * // Raw: + * await client.postBotCommands([ + * { + * options: [], + * name: 'test', + * name_localizations: null, + * description: 'command description', + * description_localizations: null, + * contexts: [], + * default_permission: null, + * default_member_permissions: null, + * dm_permission: false, + * integration_types: [], + * nsfw: false + * } + * ]); * ``` * * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON objects. This cannot be empty. From 9f88be37d6f3878832e9eee79ed1ace5d6df1a3c Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 10 Oct 2025 17:25:58 +0700 Subject: [PATCH 26/48] feat: rename postBotCommands to postCommands --- README.md | 12 ++++++------ src/structs/Api.ts | 6 +++--- tests/V1Api.test.ts | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 76bdd24..1ed342c 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ const vote = await client.getVote("8226924471638491136", "topgg"); ```js const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); -await client.postBotCommands(commands); +await client.postCommands(commands); ``` ##### Eris @@ -92,7 +92,7 @@ await client.postBotCommands(commands); ```js const commands = await bot.getCommands(); -await client.postBotCommands(commands); +await client.postCommands(commands); ``` ##### Discordeno @@ -102,7 +102,7 @@ import { getApplicationCommands } from "discordeno"; const commands = await getApplicationCommands(bot); -await client.postBotCommands(commands); +await client.postCommands(commands); ``` ##### Harmony @@ -110,7 +110,7 @@ await client.postBotCommands(commands); ```js const commands = await bot.interactions.commands.all(); -await client.postBotCommands(commands); +await client.postCommands(commands); ``` ##### Oceanic @@ -118,13 +118,13 @@ await client.postBotCommands(commands); ```js const commands = await bot.application.getGlobalCommands(); -await client.postBotCommands(commands); +await client.postCommands(commands); ``` ##### Raw ```js -await client.postBotCommands([ +await client.postCommands([ { options: [], name: 'test', diff --git a/src/structs/Api.ts b/src/structs/Api.ts index d2a95f2..da2bf86 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -319,10 +319,10 @@ export class V1Api extends Api { * // Oceanic: * const commands = await bot.application.getGlobalCommands(); * - * await client.postBotCommands(commands); + * await client.postCommands(commands); * * // Raw: - * await client.postBotCommands([ + * await client.postCommands([ * { * options: [], * name: 'test', @@ -341,7 +341,7 @@ export class V1Api extends Api { * * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON objects. This cannot be empty. */ - public async postBotCommands(commands: APIApplicationCommand[]): Promise { + public async postCommands(commands: APIApplicationCommand[]): Promise { await this._request("POST", "/v1/projects/@me/commands", commands); } diff --git a/tests/V1Api.test.ts b/tests/V1Api.test.ts index da6a3a8..eeff856 100644 --- a/tests/V1Api.test.ts +++ b/tests/V1Api.test.ts @@ -4,9 +4,9 @@ import { VOTE } from './mocks/data'; /* mock token */ const client = new V1Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); -describe('API postBotCommands test', () => { - it('postBotCommands should work', () => { - expect(client.postBotCommands([{ +describe('API postCommands test', () => { + it('postCommands should work', () => { + expect(client.postCommands([{ id: '1', type: 1, application_id: '1', From eb64cb1c8e47f17be08ae03fed43420c989ae31e Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 10 Oct 2025 17:42:17 +0700 Subject: [PATCH 27/48] doc: readme tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ed342c..3cb7a41 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ $ yarn add @top-gg/sdk ### API v1 -Note that API v1 also includes API v0. +> **NOTE**: API v1 also includes API v0. ```js import Topgg from "@top-gg/sdk"; From 2c159e0e7d53da6c6e69cd2f95f554844cb7e861 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:08:52 +0700 Subject: [PATCH 28/48] doc: fix API version links in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3cb7a41..1034499 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ The community-maintained Node.js library for Top.gg. - [Installation](#installation) - [Setting up](#setting-up) - [Usage](#usage) - - [API v1](#api-v1) + - [API v1](#api-v1-1) - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) - - [API v0](#api-v0) + - [API v0](#api-v0-1) - [Getting a bot](#getting-a-bot) - [Getting several bots](#getting-several-bots) - [Getting your project's voters](#getting-your-projects-voters) @@ -269,4 +269,4 @@ app.post("/votes", webhook.listener(vote => { })); app.listen(8080); -``` \ No newline at end of file +``` From 40cae1c693599e30bfd7c945a8c8af4f641727d5 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:23:04 +0700 Subject: [PATCH 29/48] doc: remove for more information link from README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1034499..dab0c59 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Top.gg Node.js SDK -> For more information, see the documentation here: https://topgg.js.org. - The community-maintained Node.js library for Top.gg. ## Chapters From 7d342ca224f52cd7c05d93d586786aa1b79456fb Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:14:48 +0700 Subject: [PATCH 30/48] revert: revert changes to the GitHub Workflows and eslint rules --- .eslintrc.js | 7 +++ .github/workflows/build.yml | 6 +-- .github/workflows/publish.yml | 4 +- .github/workflows/test.yml | 4 +- eslint.config.js | 81 ----------------------------------- 5 files changed, 14 insertions(+), 88 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 eslint.config.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..fbe2908 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + ignorePatterns: ["node_modules/*", "docs/*", "dist/*"], + extends: "@top-gg/eslint-config", + parserOptions: { + project: "./tsconfig.json", + } +}; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c3e42f..6232ab2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,9 @@ jobs: matrix: node: [18, 20] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v3 - name: Cache node_modules - uses: actions/cache@v4 + uses: actions/cache@v3 env: cache-name: cache-node-modules with: @@ -26,7 +26,7 @@ jobs: ${{ runner.os }}-build- ${{ runner.os }}- - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} check-latest: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d545b16..2d6d3e7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,8 +8,8 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: 18 check-latest: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65d32d1..bce1efc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,9 +10,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v3 with: node-version: 18 check-latest: true diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 7e89370..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,81 +0,0 @@ -const js = require('@eslint/js'); -const ts = require("@typescript-eslint/eslint-plugin"); - -const tsPlugin = require('@typescript-eslint/eslint-plugin'); -const jestPlugin = require('eslint-plugin-jest'); - -module.exports = [ - { - ignores: ["node_modules/*", "docs/*", "dist/*"] - }, - { - ...js.configs.recommended, - files: ["src/**/*.ts"], - languageOptions: { - parser: require('@typescript-eslint/parser'), - parserOptions: { - project: './tsconfig.json', - }, - globals: { - es6: true, - browser: true, - node: true, - jest: true - } - }, - plugins: { - "@typescript-eslint": tsPlugin, - jest: jestPlugin - }, - rules: { - ...ts.configs.recommended.rules, - semi: "error", - "no-unreachable-loop": "warn", - "no-unsafe-optional-chaining": "warn", - eqeqeq: "error", - "no-alert": "error", - "prefer-spread": "error", - "no-duplicate-imports": "warn", - "no-eval": "error", - "no-implied-eval": "error", - "no-extend-native": "warn", - "no-new-wrappers": "error", - "no-proto": "error", - "no-script-url": "error", - "no-self-compare": "warn", - "no-useless-catch": "warn", - "no-throw-literal": "error", - "no-var": "warn", - "no-labels": "error", - "no-undefined": "off", - "no-new-object": "error", - "no-multi-assign": "warn", - "prefer-const": "warn", - "prefer-numeric-literals": "warn", - "prefer-object-spread": "error", - "prefer-rest-params": "error", - "prefer-exponentiation-operator": "error", - "no-lonely-if": "error", - radix: "warn", - camelcase: "warn", - "new-cap": "error", - quotes: ["warn", "double", { allowTemplateLiterals: true }], - "no-void": "error", - "spaced-comment": ["warn", "always"], - "eol-last": "warn", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/prefer-optional-chain": "error", - "@typescript-eslint/prefer-for-of": "error", - "@typescript-eslint/no-namespace": [ - "error", - { allowDefinitionFiles: true } - ] - } - }, - { - files: ["*.browser.js"], - env: { - browser: true - } - } -]; From 4572c80c0aaf55f504a04a7a2d390c80b80a2449 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:44:59 +0700 Subject: [PATCH 31/48] revert: revert Authorization header change --- src/structs/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index da2bf86..7c0e2fe 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -72,7 +72,7 @@ export class Api extends EventEmitter { body?: Record ): Promise { const headers: IncomingHttpHeaders = {}; - if (this.options.token) headers["authorization"] = `Bearer ${this.options.token}`; + if (this.options.token) headers["authorization"] = this.options.token; if (method !== "GET") headers["content-type"] = "application/json"; let url = `https://top.gg/api${path}`; From 6768ba10f6c080d268408e38f23cd8d910961610 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:51:39 +0700 Subject: [PATCH 32/48] revert: revert back to TopGGAPIError --- src/structs/Api.ts | 8 ++++---- src/utils/ApiError.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 7c0e2fe..003d8a9 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -1,7 +1,7 @@ import type { APIApplicationCommand } from "discord-api-types/v10"; import type { IncomingHttpHeaders } from "undici/types/header"; import { request, type Dispatcher } from "undici"; -import APIError from "../utils/ApiError"; +import TopGGAPIError from "../utils/ApiError"; import { EventEmitter } from "events"; import { STATUS_CODES } from "http"; @@ -98,7 +98,7 @@ export class Api extends EventEmitter { } if (response.statusCode < 200 || response.statusCode > 299) { - throw new APIError( + throw new TopGGAPIError( response.statusCode, STATUS_CODES[response.statusCode] ?? "", responseBody @@ -195,7 +195,7 @@ export class Api extends EventEmitter { * @returns {UserInfo} Info for user */ // eslint-disable-next-line @typescript-eslint/no-unused-vars public async getUser(_id: Snowflake): Promise { - throw new APIError( + throw new TopGGAPIError( 404, STATUS_CODES[404]!, "getUser is no longer supported by Top.gg API v0." @@ -374,7 +374,7 @@ export class V1Api extends Api { weight: response.weight }; } catch (err) { - const topggError = err as APIError; + const topggError = err as TopGGAPIError; if (topggError.statusCode === 404) { return null; diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index 8cca887..5a8bd1c 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -4,7 +4,7 @@ const tips = { }; /** API Error */ -export default class APIError extends Error { +export default class TopGGAPIError extends Error { /** Response status code */ public statusCode: number; From 3059297cc1d241e5e18db026190aa626c60ed832 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:54:37 +0700 Subject: [PATCH 33/48] revert: revert dependency version bumps --- package.json | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index f2e0afe..1bb69ce 100644 --- a/package.json +++ b/package.json @@ -25,28 +25,27 @@ }, "homepage": "https://topgg.js.org", "devDependencies": { - "@eslint/js": "^9.31.0", - "@types/express": "^5.0.3", - "@types/jest": "^30.0.0", - "@types/node": "^24.0.14", - "@typescript-eslint/eslint-plugin": "^8.37.0", - "@typescript-eslint/parser": "^8.37.0", - "discord-api-types": "^0.38.23", - "eslint": "^9.31.0", - "eslint-config-prettier": "^10.1.5", - "eslint-plugin-jest": "^29.0.1", - "express": "^5.1.0", - "husky": "^9.1.7", - "jest": "^30.0.4", - "lint-staged": "^16.1.2", - "prettier": "^3.6.2", - "ts-jest": "^29.4.0", - "typedoc": "^0.28.7", - "typescript": "^5.8.3" + "@top-gg/eslint-config": "^0.0.4", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.4", + "@types/node": "^20.5.9", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "eslint": "^8.48.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-jest": "^27.2.3", + "express": "^4.18.2", + "husky": "^8.0.3", + "jest": "^29.6.4", + "lint-staged": "^14.0.1", + "prettier": "^3.0.3", + "ts-jest": "^29.1.1", + "typedoc": "^0.25.1", + "typescript": "^5.2.2" }, "dependencies": { - "raw-body": "^3.0.0", - "undici": "^7.11.0" + "raw-body": "^2.5.2", + "undici": "^5.23.0" }, "types": "./dist/index.d.ts" } From e1226fd75f52eb57b18d173ac60c00283d77c3e0 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:57:42 +0700 Subject: [PATCH 34/48] revert: revert script updates --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1bb69ce..943ebab 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "build:ci": "npm i --include=dev && tsc", "docs": "typedoc", "prepublishOnly": "npm run build:ci", - "lint": "eslint", - "lint:ci": "eslint --output-file eslint_report.json --format json", + "lint": "eslint src/**/*.ts", + "lint:ci": "eslint --output-file eslint_report.json --format json src/**/*.ts", "prepare": "npx husky install" }, "repository": { From 0871797804a7d795676843682c3b92c0b04b7310 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:03:37 +0700 Subject: [PATCH 35/48] fix: add discord-api-types to dev dependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 943ebab..a44328c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/node": "^20.5.9", "@typescript-eslint/eslint-plugin": "^6.6.0", "@typescript-eslint/parser": "^6.6.0", + "discord-api-types": "^0.38.38", "eslint": "^8.48.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.2.3", From 5f7004772822c41ee4948ad9c89a4b0bddefc70c Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:30:44 +0700 Subject: [PATCH 36/48] revert: revert changes to the error class --- src/structs/Api.ts | 17 +++++++++-------- src/utils/ApiError.ts | 17 ++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 003d8a9..3ee3f47 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -17,6 +17,7 @@ import { UserInfo, UserSource, } from "../typings"; +import { Readable } from "stream"; /** * Top.gg API v0 client @@ -101,7 +102,7 @@ export class Api extends EventEmitter { throw new TopGGAPIError( response.statusCode, STATUS_CODES[response.statusCode] ?? "", - responseBody + response ); } @@ -193,13 +194,13 @@ export class Api extends EventEmitter { * * @param {Snowflake} _id User ID * @returns {UserInfo} Info for user - */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async getUser(_id: Snowflake): Promise { - throw new TopGGAPIError( - 404, - STATUS_CODES[404]!, - "getUser is no longer supported by Top.gg API v0." + */ + public async getUser(id: Snowflake): Promise { + console.warn( + "[DeprecationWarning] getUser is no longer supported by Top.gg API v0." ); + + return this._request("GET", `/users/${id}`); } /** @@ -376,7 +377,7 @@ export class V1Api extends Api { } catch (err) { const topggError = err as TopGGAPIError; - if (topggError.statusCode === 404) { + if (topggError.response?.statusCode === 404) { return null; } diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index 5a8bd1c..b4df924 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -1,3 +1,5 @@ +import type { Dispatcher } from "undici"; + const tips = { 401: "You need a token for this endpoint", 403: "You don't have access to this endpoint", @@ -5,21 +7,14 @@ const tips = { /** API Error */ export default class TopGGAPIError extends Error { - /** Response status code */ - public statusCode: number; - - /** Possible response body from Response */ - public body?: string | object; - - constructor(code: number, text: string, body?: string | object) { + /** Possible response from Request */ + public response?: Dispatcher.ResponseData; + constructor(code: number, text: string, response: Dispatcher.ResponseData) { if (code in tips) { super(`${code} ${text} (${tips[code as keyof typeof tips]})`); } else { super(`${code} ${text}`); } - - this.statusCode = code; - this.message = tips[code as keyof typeof tips] ?? text; - this.body = body; + this.response = response; } } From bc9011fbf5db03947621a6fbaab420ebe44fb1fc Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:33:47 +0700 Subject: [PATCH 37/48] fix: remove unused import --- src/structs/Api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 3ee3f47..c713f98 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -17,7 +17,6 @@ import { UserInfo, UserSource, } from "../typings"; -import { Readable } from "stream"; /** * Top.gg API v0 client From ecd1dc1ccb9d401adc468dd99e19cbe1406b3a95 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:01:44 +0700 Subject: [PATCH 38/48] doc: minor readme tweaks --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dab0c59..02b8354 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Top.gg Node.js SDK +> For more information, see the documentation here: https://topgg.js.org. + The community-maintained Node.js library for Top.gg. ## Chapters @@ -7,10 +9,10 @@ The community-maintained Node.js library for Top.gg. - [Installation](#installation) - [Setting up](#setting-up) - [Usage](#usage) - - [API v1](#api-v1-1) + - [API v1](#api-v1) - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) - - [API v0](#api-v0-1) + - [API v0](#api-v0) - [Getting a bot](#getting-a-bot) - [Getting several bots](#getting-several-bots) - [Getting your project's voters](#getting-your-projects-voters) @@ -23,8 +25,10 @@ The community-maintained Node.js library for Top.gg. - [Webhooks](#webhooks) - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) + ## Installation + ### NPM ```sh @@ -39,9 +43,12 @@ $ yarn add @top-gg/sdk ## Setting up -### API v1 -> **NOTE**: API v1 also includes API v0. +### v1 + +:::note +API v1 also includes API v0. +::: ```js import Topgg from "@top-gg/sdk"; @@ -49,7 +56,7 @@ import Topgg from "@top-gg/sdk"; const client = new Topgg.V1Api(process.env.TOPGG_TOKEN); ``` -### API v0 +### v0 ```js import Topgg from "@top-gg/sdk"; @@ -63,6 +70,7 @@ const client = new Topgg.Api(process.env.TOPGG_TOKEN); #### Getting your project's vote information of a user + ##### Discord ID ```js @@ -77,6 +85,7 @@ const vote = await client.getVote("8226924471638491136", "topgg"); #### Posting your bot's application commands list + ##### Discord.js ```js @@ -155,6 +164,7 @@ const bots = await client.getBots(); #### Getting your project's voters + ##### First page ```js @@ -164,6 +174,7 @@ const voters = await client.getVoters(); ##### Subsequent pages ```js +// Page number const voters = await client.getVoters(2); ``` @@ -192,6 +203,7 @@ await api.postStats({ You would need to use the third-party `topgg-autoposter` package to be able to autopost. Install it in your terminal like so: + ##### NPM ```sh @@ -225,6 +237,7 @@ const isWeekend = await client.isWeekend(); #### Generating widget URLs + ##### Large ```js @@ -267,4 +280,4 @@ app.post("/votes", webhook.listener(vote => { })); app.listen(8080); -``` +``` \ No newline at end of file From 2aef3eb68a443835ffb65a099481d08d8da9baa6 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:15:11 +0700 Subject: [PATCH 39/48] doc: fix mismatched jsdoc argument name --- src/structs/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index c713f98..1e3a231 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -191,7 +191,7 @@ export class Api extends EventEmitter { * user.username; // Xignotic * ``` * - * @param {Snowflake} _id User ID + * @param {Snowflake} id User ID * @returns {UserInfo} Info for user */ public async getUser(id: Snowflake): Promise { From bb846445357272ca738f334a6da954c8d7712c94 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:01:33 +0700 Subject: [PATCH 40/48] feat: delete v0 APIs --- README.md | 169 +++----------------------------- package.json | 2 +- src/structs/Api.ts | 201 +------------------------------------- src/typings.ts | 206 --------------------------------------- tests/Api.test.ts | 73 ++++---------- tests/V1Api.test.ts | 29 ------ tests/mocks/data.ts | 54 +--------- tests/mocks/endpoints.ts | 54 +--------- 8 files changed, 35 insertions(+), 753 deletions(-) delete mode 100644 tests/V1Api.test.ts diff --git a/README.md b/README.md index 02b8354..5ad9d55 100644 --- a/README.md +++ b/README.md @@ -9,23 +9,11 @@ The community-maintained Node.js library for Top.gg. - [Installation](#installation) - [Setting up](#setting-up) - [Usage](#usage) - - [API v1](#api-v1) - - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) - - [API v0](#api-v0) - - [Getting a bot](#getting-a-bot) - - [Getting several bots](#getting-several-bots) - - [Getting your project's voters](#getting-your-projects-voters) - - [Check if a user has voted for your project](#check-if-a-user-has-voted-for-your-project) - - [Getting your bot's statistics](#getting-your-bots-statistics) - - [Posting your bot's statistics](#posting-your-bots-statistics) - - [Automatically posting your bot's statistics every few minutes](#automatically-posting-your-bots-statistics-every-few-minutes) - - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) - - [Generating widget URLs](#generating-widget-urls) + - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) + - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) - [Webhooks](#webhooks) - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) - ## Installation @@ -43,21 +31,6 @@ $ yarn add @top-gg/sdk ## Setting up - -### v1 - -:::note -API v1 also includes API v0. -::: - -```js -import Topgg from "@top-gg/sdk"; - -const client = new Topgg.V1Api(process.env.TOPGG_TOKEN); -``` - -### v0 - ```js import Topgg from "@top-gg/sdk"; @@ -66,27 +39,23 @@ const client = new Topgg.Api(process.env.TOPGG_TOKEN); ## Usage -### API v1 - -#### Getting your project's vote information of a user +### Getting your project's vote information of a user - -##### Discord ID +#### Discord ID ```js const vote = await client.getVote("661200758510977084"); ``` -##### Top.gg ID +#### Top.gg ID ```js const vote = await client.getVote("8226924471638491136", "topgg"); ``` -#### Posting your bot's application commands list - +### Posting your bot's application commands list -##### Discord.js +#### Discord.js ```js const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON()); @@ -94,7 +63,7 @@ const commands = (await bot.application.commands.fetch()).map(cmd => cmd.toJSON( await client.postCommands(commands); ``` -##### Eris +#### Eris ```js const commands = await bot.getCommands(); @@ -102,7 +71,7 @@ const commands = await bot.getCommands(); await client.postCommands(commands); ``` -##### Discordeno +#### Discordeno ```js import { getApplicationCommands } from "discordeno"; @@ -112,7 +81,7 @@ const commands = await getApplicationCommands(bot); await client.postCommands(commands); ``` -##### Harmony +#### Harmony ```js const commands = await bot.interactions.commands.all(); @@ -120,7 +89,7 @@ const commands = await bot.interactions.commands.all(); await client.postCommands(commands); ``` -##### Oceanic +#### Oceanic ```js const commands = await bot.application.getGlobalCommands(); @@ -128,7 +97,7 @@ const commands = await bot.application.getGlobalCommands(); await client.postCommands(commands); ``` -##### Raw +#### Raw ```js await client.postCommands([ @@ -148,120 +117,6 @@ await client.postCommands([ ]); ``` -### API v0 - -#### Getting a bot - -```js -const bot = await client.getBot("461521980492087297"); -``` - -#### Getting several bots - -```js -const bots = await client.getBots(); -``` - -#### Getting your project's voters - - -##### First page - -```js -const voters = await client.getVoters(); -``` - -##### Subsequent pages - -```js -// Page number -const voters = await client.getVoters(2); -``` - -#### Check if a user has voted for your project - -```js -const hasVoted = await client.hasVoted("661200758510977084"); -``` - -#### Getting your bot's statistics - -```js -const stats = await client.getStats(); -``` - -#### Posting your bot's statistics - -```js - -await api.postStats({ - serverCount: bot.getServerCount(), -}); -``` - -#### Automatically posting your bot's statistics every few minutes - -You would need to use the third-party `topgg-autoposter` package to be able to autopost. Install it in your terminal like so: - - -##### NPM - -```sh -$ npm i topgg-autoposter -``` - -##### Yarn - -```sh -$ yarn add topgg-autoposter -``` - -Then in your code: - -```js -import { AutoPoster } from "topgg-autoposter"; - -// Your discord.js client or any other -const client = Discord.Client(); - -AutoPoster(process.env.TOPGG_TOKEN, client).on("posted", () => { - console.log("Successfully posted statistics to Top.gg!"); -}); -``` - -#### Checking if the weekend vote multiplier is active - -```js -const isWeekend = await client.isWeekend(); -``` - -#### Generating widget URLs - - -##### Large - -```js -const widgetUrl = Topgg.Widget.large(Topgg.WidgetType.DiscordBot, "574652751745777665"); -``` - -##### Votes - -```js -const widgetUrl = Topgg.Widget.votes(Topgg.WidgetType.DiscordBot, "574652751745777665"); -``` - -##### Owner - -```js -const widgetUrl = Topgg.Widget.owner(Topgg.WidgetType.DiscordBot, "574652751745777665"); -``` - -##### Social - -```js -const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745777665"); -``` - ### Webhooks #### Being notified whenever someone voted for your project diff --git a/package.json b/package.json index a44328c..9fce006 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@top-gg/sdk", - "version": "3.2.0", + "version": "4.0.0", "description": "A community-maintained Node.js API Client for the Top.gg API.", "main": "./dist/index.js", "scripts": { diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 1e3a231..2a8ab08 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -8,18 +8,12 @@ import { STATUS_CODES } from "http"; import { APIOptions, Snowflake, - BotInfo, - BotsResponse, - BotStats, - ShortUser, - BotsQuery, Vote, - UserInfo, UserSource, } from "../typings"; /** - * Top.gg API v0 client + * Top.gg API v1 client * * @example * ```js @@ -32,7 +26,7 @@ import { * @link {@link https://docs.top.gg | API Reference} */ export class Api extends EventEmitter { - protected options: APIOptions; + private options: APIOptions; /** * Create Top.gg API instance @@ -66,7 +60,7 @@ export class Api extends EventEmitter { }; } - protected async _request( + private async _request( method: Dispatcher.HttpMethod, path: string, body?: Record @@ -108,195 +102,6 @@ export class Api extends EventEmitter { return responseBody; } - /** - * Post your bot's stats to Top.gg - * - * @example - * ```js - * await api.postStats({ - * serverCount: 28199, - * }); - * ``` - * - * @param {object} stats Stats object - * @param {number} stats.serverCount Server count - * @returns {BotStats} Passed object - */ - public async postStats(stats: BotStats): Promise { - if ((stats?.serverCount ?? 0) <= 0) throw new Error("Missing server count"); - - /* eslint-disable camelcase */ - await this._request("POST", "/bots/stats", { - server_count: stats.serverCount, - }); - /* eslint-enable camelcase */ - - return stats; - } - - /** - * Get your bot's stats - * - * @example - * ```js - * await api.getStats(); - * // => - * { - * serverCount: 28199, - * shardCount: null, - * shards: [] - * } - * ``` - * - * @returns {BotStats} Your bot's stats - */ - public async getStats(_id?: Snowflake): Promise { - if (_id) - console.warn( - "[DeprecationWarning] getStats() no longer needs an ID argument" - ); - const result = await this._request("GET", "/bots/stats"); - return { - serverCount: result.server_count, - shardCount: null, - shards: [], - }; - } - - /** - * Get bot info - * - * @example - * ```js - * const bot = await client.getBot("461521980492087297"); - * ``` - * - * @param {Snowflake} id Bot ID - * @returns {BotInfo} Info for bot - */ - public async getBot(id: Snowflake): Promise { - if (!id) throw new Error("ID Missing"); - return this._request("GET", `/bots/${id}`); - } - - /** - * @deprecated No longer supported by Top.gg API v0. - * - * Get user info - * - * @example - * ```js - * await api.getUser("205680187394752512"); - * // => - * user.username; // Xignotic - * ``` - * - * @param {Snowflake} id User ID - * @returns {UserInfo} Info for user - */ - public async getUser(id: Snowflake): Promise { - console.warn( - "[DeprecationWarning] getUser is no longer supported by Top.gg API v0." - ); - - return this._request("GET", `/users/${id}`); - } - - /** - * Get a list of bots - * - * @example - * ```js - * const bots = await client.getBots(); - * ``` - * - * @param {BotsQuery} query Bot Query - * @returns {BotsResponse} Return response - */ - public async getBots(query?: BotsQuery): Promise { - if (query) { - if (Array.isArray(query.fields)) query.fields = query.fields.join(", "); - } - return this._request("GET", "/bots", query); - } - - /** - * Get recent 100 unique voters - * - * @example - * ```js - * // First page - * const voters1 = await client.getVoters(); - * - * // Subsequent pages - * const voters2 = await client.getVoters(2); - * ``` - * - * @param {number} [page] The page number. Page numbers start at 1. Each page can only have at most 100 voters. - * @returns {ShortUser[]} Array of 100 unique voters - */ - public async getVotes(page?: number): Promise { - return this._request("GET", `/bots/${this.options.id}/votes`, { page: page ?? 1 }); - } - - /** - * Get whether or not a user has voted in the last 12 hours - * - * @example - * ```js - * const hasVoted = await client.hasVoted("661200758510977084"); - * ``` - * - * @param {Snowflake} id User ID - * @returns {boolean} Whether the user has voted in the last 12 hours - */ - public async hasVoted(id: Snowflake): Promise { - if (!id) throw new Error("Missing ID"); - - return this._request("GET", "/bots/check", { userId: id }).then( - (x) => !!x.voted - ); - } - - /** - * Whether or not the weekend multiplier is active - * - * @example - * ```js - * const isWeekend = await client.isWeekend(); - * ``` - * - * @returns {boolean} Whether the multiplier is active - */ - public async isWeekend(): Promise { - return this._request("GET", "/weekend").then((x) => x.is_weekend); - } -} - -/** - * Top.gg API v1 client - * - * @example - * ```js - * const Topgg = require("@top-gg/sdk"); - * - * const client = new Topgg.V1Api(process.env.TOPGG_TOKEN); - * ``` - * - * @link {@link https://topgg.js.org | Library docs} - * @link {@link https://docs.top.gg | API Reference} - */ -export class V1Api extends Api { - /** - * Create Top.gg API instance - * - * @param {string} token Token or options - * @param {APIOptions} [options] API Options - */ - constructor(token: string, options: APIOptions = {}) { - super(token, options); - } - /** * Updates the application commands list in your Discord bot's Top.gg page. * diff --git a/src/typings.ts b/src/typings.ts index a0b17c0..e9d5925 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -12,197 +12,6 @@ export interface APIOptions { /** A user account from an external platform that is linked to a Top.gg user account. */ export type UserSource = "discord" | "topgg"; -export interface BotInfo { - /** The Top.gg ID of the bot */ - id: Snowflake; - /** The Discord ID of the bot */ - clientid: Snowflake; - /** The username of the bot */ - username: string; - /** - * The discriminator of the bot - * - * @deprecated No longer supported by Top.gg API v0. - */ - discriminator: string; - /** The bot's avatar */ - avatar: string; - /** - * The cdn hash of the bot's avatar if the bot has none - * - * @deprecated No longer supported by Top.gg API v0. - */ - defAvatar: string; - /** - * The URL for the banner image - * - * @deprecated No longer supported by Top.gg API v0. - */ - bannerUrl?: string; - /** - * The library of the bot - * - * @deprecated No longer supported by Top.gg API v0. - */ - lib: string; - /** The prefix of the bot */ - prefix: string; - /** The short description of the bot */ - shortdesc: string; - /** The long description of the bot. Can contain HTML and/or Markdown */ - longdesc?: string; - /** The tags of the bot */ - tags: string[]; - /** The website url of the bot */ - website?: string; - /** The support url of the bot */ - support?: string; - /** The link to the github repo of the bot */ - github?: string; - /** The owners of the bot. First one in the array is the main owner */ - owners: Snowflake[]; - /** - * The guilds featured on the bot page - * - * @deprecated No longer supported by Top.gg API v0. - */ - guilds: Snowflake[]; - /** The custom bot invite url of the bot */ - invite?: string; - /** The date when the bot was submitted (in ISO 8601) */ - date: string; - /** - * The certified status of the bot - * - * @deprecated No longer supported by Top.gg API v0. - */ - certifiedBot: boolean; - /** The vanity url of the bot */ - vanity?: string; - /** The amount of votes the bot has */ - points: number; - /** The amount of votes the bot has this month */ - monthlyPoints: number; - /** - * The guild id for the donatebot setup - * - * @deprecated No longer supported by Top.gg API v0. - */ - donatebotguildid: Snowflake; - /** The amount of servers the bot is in based on posted stats */ - server_count?: number; - /** The bot's reviews on Top.gg */ - reviews: { - /** This bot's average review score out of 5 */ - averageScore: number; - /** This bot's review count */ - count: number; - }; -} - -export interface BotStats { - /** The amount of servers the bot is in */ - serverCount?: number; - /** - * The amount of servers the bot is in per shard. Always present but can be - * empty. (Only when receiving stats) - * - * @deprecated No longer supported by Top.gg API v0. - */ - shards?: number[]; - /** - * The shard ID to post as (only when posting) - * - * @deprecated No longer supported by Top.gg API v0. - */ - shardId?: number; - /** - * The amount of shards a bot has - * - * @deprecated No longer supported by Top.gg API v0. - */ - shardCount?: number | null; -} - -/** - * @deprecated No longer supported by Top.gg API v0. - */ -export interface UserInfo { - /** The id of the user */ - id: Snowflake; - /** The username of the user */ - username: string; - /** The discriminator of the user */ - discriminator: string; - /** The user's avatar url */ - avatar: string; - /** The cdn hash of the user's avatar if the user has none */ - defAvatar: string; - /** The bio of the user */ - bio?: string; - /** The banner image url of the user */ - banner?: string; - /** The social usernames of the user */ - social: { - /** The youtube channel id of the user */ - youtube?: string; - /** The reddit username of the user */ - reddit?: string; - /** The twitter username of the user */ - twitter?: string; - /** The instagram username of the user */ - instagram?: string; - /** The github username of the user */ - github?: string; - }; - /** The custom hex color of the user */ - color: string; - /** The supporter status of the user */ - supporter: boolean; - /** The certified status of the user */ - certifiedDev: boolean; - /** The mod status of the user */ - mod: boolean; - /** The website moderator status of the user */ - webMod: boolean; - /** The admin status of the user */ - admin: boolean; -} - -export interface BotsQuery { - /** The amount of bots to return. Max. 500 */ - limit?: number; - /** Amount of bots to skip */ - offset?: number; - /** - * A search string in the format of "field: value field2: value2" - * - * @deprecated No longer supported by Top.gg API v0. - */ - search?: - | { - [key in keyof BotInfo]: string; - } - | string; - /** Sorts results from a specific criteria. Results will always be descending. */ - sort?: "monthlyPoints" | "id" | "date"; - /** A list of fields to show. */ - fields?: string[] | string; -} - -export interface BotsResponse { - /** The matching bots */ - results: BotInfo[]; - /** The limit used */ - limit: number; - /** The offset used */ - offset: number; - /** The length of the results array */ - count: number; - /** The total number of bots matching your search */ - total: number; -} - export interface Vote { /** When the vote was cast */ votedAt?: string; @@ -212,21 +21,6 @@ export interface Vote { weight?: number; } -export interface ShortUser { - /** User's ID */ - id: Snowflake; - /** User's username */ - username: string; - /** - * User's discriminator - * - * @deprecated No longer supported by Top.gg API v0. - */ - discriminator: string; - /** User's avatar url */ - avatar: string; -} - export interface WebhookPayload { /** If webhook is a Discord bot: ID of the bot that received a vote */ bot?: Snowflake; diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 89259e7..90ccb23 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,68 +1,29 @@ import { Api } from '../src/index'; -import ApiError from '../src/utils/ApiError'; -import { BOT, BOT_STATS, VOTES } from './mocks/data'; +import { VOTE } from './mocks/data'; /* mock token */ const client = new Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); -describe('API postStats test', () => { - it('postStats without server count should throw error', async () => { - await expect(client.postStats({ shardCount: 0 })).rejects.toThrow(Error); - }); - - it('postStats with invalid negative server count should throw error', () => { - expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); - }); - - it('postStats should return 200', async () => { - await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( - Object - ); - }); -}); - -describe('API getStats test', () => { - it('getStats should return 200 when bot is found', async () => { - expect(client.getStats()).resolves.toStrictEqual({ - serverCount: BOT_STATS.server_count, - shardCount: BOT_STATS.shard_count, - shards: BOT_STATS.shards - }); +describe('API postCommands test', () => { + it('postCommands should work', () => { + expect(client.postCommands([{ + id: '1', + type: 1, + application_id: '1', + name: 'test', + description: 'command description', + default_member_permissions: '', + version: '1' + }])).resolves.toBeUndefined(); }); }); -describe('API getBot test', () => { - it('getBot should return 404 when bot is not found', () => { - expect(client.getBot('0')).rejects.toThrow(ApiError); +describe('API getVote test', () => { + it('getVote should return 200 when token is provided', () => { + expect(client.getVote('1')).resolves.toStrictEqual(VOTE); }); - it('getBot should return 200 when bot is found', async () => { - expect(client.getBot('1')).resolves.toStrictEqual(BOT); - }); - - it('getBot should throw when no id is provided', () => { - expect(client.getBot('')).rejects.toThrow(Error); + it('getVote should throw error when no id is provided', () => { + expect(client.getVote('')).rejects.toThrow(Error); }); }); - -describe('API getVotes test', () => { - it('getVotes should return 200 when token is provided', () => { - expect(client.getVotes()).resolves.toEqual(VOTES); - }); -}); - -describe('API hasVoted test', () => { - it('hasVoted should return 200 when token is provided', () => { - expect(client.hasVoted('1')).resolves.toBe(true); - }); - - it('hasVoted should throw error when no id is provided', () => { - expect(client.hasVoted('')).rejects.toThrow(Error); - }); -}); - -describe('API isWeekend tests', () => { - it('isWeekend should return true', async () => { - expect(client.isWeekend()).resolves.toBe(true); - }); -}); \ No newline at end of file diff --git a/tests/V1Api.test.ts b/tests/V1Api.test.ts deleted file mode 100644 index eeff856..0000000 --- a/tests/V1Api.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { V1Api } from '../src/index'; -import { VOTE } from './mocks/data'; - -/* mock token */ -const client = new V1Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); - -describe('API postCommands test', () => { - it('postCommands should work', () => { - expect(client.postCommands([{ - id: '1', - type: 1, - application_id: '1', - name: 'test', - description: 'command description', - default_member_permissions: '', - version: '1' - }])).resolves.toBeUndefined(); - }); -}); - -describe('API getVote test', () => { - it('getVote should return 200 when token is provided', () => { - expect(client.getVote('1')).resolves.toStrictEqual(VOTE); - }); - - it('getVote should throw error when no id is provided', () => { - expect(client.getVote('')).rejects.toThrow(Error); - }); -}); diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index 9f68ed0..b47059c 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -1,36 +1,3 @@ -// https://docs.top.gg/api/bot/#find-one-bot -export const BOT = { - invite: "https://top.gg/discord", - support: "https://discord.gg/dbl", - github: "https://github.com/top-gg", - longdesc: - "A bot to grant API access to our Library Developers on the Top.gg site without them needing to submit a bot to pass verification just to be able to access the API. \n" + - "\n" + - "Access to this bot's team can be requested by contacting a Community Manager in [our Discord server](https://top.gg/discord).", - shortdesc: "API access for Top.gg Library Developers", - prefix: "/", - clientid: "1026525568344264724", - avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", - id: "1026525568344264724", - username: "Top.gg Lib Dev API Access", - date: "2022-10-03T16:08:55.000Z", - server_count: 2, - monthlyPoints: 4, - points: 18, - owners: ["491002268401926145"], - tags: ["api", "library", "topgg"], - reviews: { averageScore: 5, count: 2 } -} - -// https://docs.top.gg/api/bot/#search-bots -export const BOTS = { - limit: 0, - offset: 0, - count: 1, - total: 1, - results: [BOT], -} - export const RAW_VOTE = { created_at: "2025-09-09T08:55:16.218761+00:00", expires_at: "2025-09-09T20:55:16.218761+00:00", @@ -43,28 +10,9 @@ export const VOTE = { weight: 1 }; -// https://docs.top.gg/api/bot/#last-1000-votes -export const VOTES = [ - { - username: "Xetera", - id: "140862798832861184", - avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" - } -] - // https://docs.top.gg/api/bot/#bot-stats export const BOT_STATS = { server_count: 0, shards: [], shard_count: null -} - -// https://docs.top.gg/api/bot/#individual-user-vote -export const USER_VOTE = { - voted: 1 -} - -// Undocumented 😢 -export const WEEKEND = { - is_weekend: true -} +} \ No newline at end of file diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index c7e8b62..3a8843f 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -1,60 +1,8 @@ import { MockInterceptor } from 'undici/types/mock-interceptor'; -import { BOT, BOTS, BOT_STATS, RAW_VOTE, USER_VOTE, VOTES, WEEKEND } from './data'; +import { RAW_VOTE } from './data'; import { getIdInPath } from '../jest.setup'; export const endpoints = [ - { - pattern: '/api/bots', - method: 'GET', - data: BOTS, - requireAuth: true - }, - { - pattern: '/api/bots/:bot_id', - method: 'GET', - data: BOT, - requireAuth: true, - validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/bots/:bot_id', request.path); - if (Number(bot_id) === 0) return { statusCode: 404 }; - return null; - } - }, - { - pattern: '/api/bots/:bot_id/votes', - method: 'GET', - data: VOTES, - requireAuth: true, - validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/bots/:bot_id/votes', request.path); - if (Number(bot_id) === 0) return { statusCode: 404 }; - return null; - } - }, - { - pattern: '/api/bots/check', - method: 'GET', - data: USER_VOTE, - requireAuth: true - }, - { - pattern: '/api/bots/stats', - method: 'GET', - data: BOT_STATS, - requireAuth: true - }, - { - pattern: '/api/bots/stats', - method: 'POST', - data: {}, - requireAuth: true - }, - { - pattern: '/api/weekend', - method: 'GET', - data: WEEKEND, - requireAuth: true - }, { pattern: '/api/v1/projects/@me/votes/:user_id', method: 'GET', From 7f4cd94417c52e814d364f6a459894c92267e44a Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:15:47 +0700 Subject: [PATCH 41/48] feat: add support for GET /v1/projects/@me --- src/structs/Api.ts | 33 +++++++++++++++++++++++++--- src/typings.ts | 37 +++++++++++++++++++++++++++++++ tests/Api.test.ts | 8 ++++++- tests/mocks/data.ts | 47 +++++++++++++++++++++++++++++++++++++--- tests/mocks/endpoints.ts | 8 ++++++- 5 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 2a8ab08..77b3f59 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -10,6 +10,7 @@ import { Snowflake, Vote, UserSource, + Project, } from "../typings"; /** @@ -102,6 +103,32 @@ export class Api extends EventEmitter { return responseBody; } + /** + * Gets your project's information. + * + * @returns {Promise} Your project's information. + */ + public async getSelf(): Promise { + const project = await this._request("GET", "/v1/projects/@me"); + + return { + id: project.id, + name: project.name, + platform: project.platform, + type: project.type, + headline: project.headline, + tags: project.tags, + votes: { + current: project.votes, + total: project.votes_total + }, + review: { + score: project.review_score, + count: project.review_count + } + } + } + /** * Updates the application commands list in your Discord bot's Top.gg page. * @@ -145,13 +172,14 @@ export class Api extends EventEmitter { * ``` * * @param {APIApplicationCommand[]} commands A list of application commands in raw Discord API JSON objects. This cannot be empty. + * @returns {Promise} */ public async postCommands(commands: APIApplicationCommand[]): Promise { await this._request("POST", "/v1/projects/@me/commands", commands); } /** - * Get the latest vote information of a Top.gg user on your project. + * Gets the latest vote information of a Top.gg user on your project. * * @example * ```js @@ -164,8 +192,7 @@ export class Api extends EventEmitter { * * @param {Snowflake} id The user's ID. * @param {UserSource} source The ID type to use. Defaults to "discord". - * - * @returns {Vote | null} The user's latest vote information on your project or null if the user has not voted for your project in the past 12 hours. + * @returns {Promise} The user's latest vote information on your project or null if the user has not voted for your project in the past 12 hours. */ public async getVote(id: Snowflake, source: UserSource = "discord"): Promise { if (!id) throw new Error("Missing ID"); diff --git a/src/typings.ts b/src/typings.ts index e9d5925..79adc41 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -12,6 +12,43 @@ export interface APIOptions { /** A user account from an external platform that is linked to a Top.gg user account. */ export type UserSource = "discord" | "topgg"; +/** A project's source platform */ +export type Platform = 'discord' + +/** A project's type */ +export type Type = 'bot' | 'server' + +/** A project listed on Top.gg */ +export interface Project { + /** The project's Top.gg ID */ + id: Snowflake; + /** The project's name sourced from the external platform */ + name: string; + /** The project's source platform */ + platform: Platform; + /** The project's type */ + type: Type; + /** The project's short description */ + headline: string; + /** The project's tag IDs */ + tags: string[]; + /** The project's vote information */ + votes: { + /** The project's current vote count that affects the project's ranking */ + current: number; + /** The project's total vote count */ + total: number; + }; + /** The project's review information */ + review: { + /** The project's review score out of 5 */ + score: number; + /** The project's total review count */ + count: number; + }; +} + +/** A project's vote information */ export interface Vote { /** When the vote was cast */ votedAt?: string; diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 90ccb23..a227966 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,9 +1,15 @@ import { Api } from '../src/index'; -import { VOTE } from './mocks/data'; +import { PROJECT, VOTE } from './mocks/data'; /* mock token */ const client = new Api('.eyJfdCI6IiIsImlkIjoiMzY0ODA2MDI5ODc2NTU1Nzc2In0=.'); +describe('API getSelf test', () => { + it('getSelf should work', () => { + expect(client.getSelf()).resolves.toStrictEqual(PROJECT); + }); +}); + describe('API postCommands test', () => { it('postCommands should work', () => { expect(client.postCommands([{ diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index b47059c..6bf1568 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -1,3 +1,44 @@ +export const RAW_PROJECT = { + id: '218109768489992192', + name: 'Miki', + type: 'bot', + platform: 'discord', + headline: 'A great bot with tons of features! language | admin | cards | fun | levels | roles | marriage | currency | custom commands!', + tags: [ + 'anime', + 'customizable-behavior', + 'economy', + 'fun', + 'game', + 'leveling', + 'multifunctional', + 'role-management', + 'roleplay', + 'social' + ], + votes: 1120, + votes_total: 313389, + review_score: 4.38, + review_count: 62245 +}; + +export const PROJECT = { + id: RAW_PROJECT.id, + name: RAW_PROJECT.name, + type: RAW_PROJECT.type, + platform: RAW_PROJECT.platform, + headline: RAW_PROJECT.headline, + tags: RAW_PROJECT.tags, + votes: { + current: RAW_PROJECT.votes, + total: RAW_PROJECT.votes_total + }, + review: { + score: RAW_PROJECT.review_score, + count: RAW_PROJECT.review_count + } +}; + export const RAW_VOTE = { created_at: "2025-09-09T08:55:16.218761+00:00", expires_at: "2025-09-09T20:55:16.218761+00:00", @@ -5,9 +46,9 @@ export const RAW_VOTE = { }; export const VOTE = { - votedAt: "2025-09-09T08:55:16.218761+00:00", - expiresAt: "2025-09-09T20:55:16.218761+00:00", - weight: 1 + votedAt: RAW_VOTE.created_at, + expiresAt: RAW_VOTE.expires_at, + weight: RAW_VOTE.weight }; // https://docs.top.gg/api/bot/#bot-stats diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index 3a8843f..16012ff 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -1,8 +1,14 @@ import { MockInterceptor } from 'undici/types/mock-interceptor'; -import { RAW_VOTE } from './data'; +import { RAW_PROJECT, RAW_VOTE } from './data'; import { getIdInPath } from '../jest.setup'; export const endpoints = [ + { + pattern: '/api/v1/projects/@me', + method: 'GET', + data: RAW_PROJECT, + requireAuth: true + }, { pattern: '/api/v1/projects/@me/votes/:user_id', method: 'GET', From bb1a39ef7d42026c47f97fd7373b14b41cbb21e1 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:19:50 +0700 Subject: [PATCH 42/48] style: fix eslint errors and warnings --- src/structs/Api.ts | 2 +- src/typings.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 77b3f59..a6bd4bd 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -126,7 +126,7 @@ export class Api extends EventEmitter { score: project.review_score, count: project.review_count } - } + }; } /** diff --git a/src/typings.ts b/src/typings.ts index 79adc41..d9d3a8d 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -13,10 +13,10 @@ export interface APIOptions { export type UserSource = "discord" | "topgg"; /** A project's source platform */ -export type Platform = 'discord' +export type Platform = "discord"; /** A project's type */ -export type Type = 'bot' | 'server' +export type Type = "bot" | "server"; /** A project listed on Top.gg */ export interface Project { From cd651e201eb1e92513405d2ed3b0b97988c16f0e Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 15:43:22 +0700 Subject: [PATCH 43/48] feat: implement new webhook authorization approach --- src/structs/Api.ts | 14 ++++++++---- src/structs/Webhook.ts | 48 ++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index a6bd4bd..070d658 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -13,6 +13,12 @@ import { Project, } from "../typings"; +/** The API version to use */ +export const API_VERSION = "v1"; + +/** The API's base URL */ +const BASE_URL = `https://top.gg/api/${API_VERSION}`; + /** * Top.gg API v1 client * @@ -70,7 +76,7 @@ export class Api extends EventEmitter { if (this.options.token) headers["authorization"] = this.options.token; if (method !== "GET") headers["content-type"] = "application/json"; - let url = `https://top.gg/api${path}`; + let url = BASE_URL + path; if (body && method === "GET") url += `?${new URLSearchParams(body)}`; @@ -109,7 +115,7 @@ export class Api extends EventEmitter { * @returns {Promise} Your project's information. */ public async getSelf(): Promise { - const project = await this._request("GET", "/v1/projects/@me"); + const project = await this._request("GET", "/projects/@me"); return { id: project.id, @@ -175,7 +181,7 @@ export class Api extends EventEmitter { * @returns {Promise} */ public async postCommands(commands: APIApplicationCommand[]): Promise { - await this._request("POST", "/v1/projects/@me/commands", commands); + await this._request("POST", "/projects/@me/commands", commands); } /** @@ -198,7 +204,7 @@ export class Api extends EventEmitter { if (!id) throw new Error("Missing ID"); try { - const response = await this._request("GET", `/v1/projects/@me/votes/${id}?source=${source}`); + const response = await this._request("GET", `/projects/@me/votes/${id}?source=${source}`); return { votedAt: response.created_at, diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index c5ebb1b..c7bc9c1 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -1,6 +1,8 @@ import getBody from "raw-body"; import { Request, Response, NextFunction } from "express"; +import crypto from "node:crypto"; import { WebhookPayload } from "../typings"; +import { API_VERSION } from "./Api"; export interface WebhookOptions { /** @@ -43,9 +45,9 @@ export class Webhook { /** * Create a new webhook client instance * - * @param authorization Webhook authorization to verify requests + * @param {string} authorization Webhook authorization to verify requests */ - constructor(private authorization?: string, options: WebhookOptions = {}) { + constructor(private authorization: string, options: WebhookOptions = {}) { this.options = { error: options.error ?? console.error, }; @@ -65,16 +67,40 @@ export class Webhook { res: Response ): Promise { return new Promise((resolve) => { - if ( - this.authorization && - req.headers.authorization !== this.authorization - ) - return res.status(401).json({ error: "Unauthorized" }); - // parse json - - if (req.body) return resolve(this._formatIncoming(req.body)); getBody(req, {}, (error, body) => { - if (error) return res.status(422).json({ error: "Malformed request" }); + if (error) { + res.status(422).json({ error: "Malformed request" }); + return resolve(false); + } + + let signatureHeader = req.headers["x-topgg-signature"]; + + if (Array.isArray(signatureHeader)) { + signatureHeader = signatureHeader[0]; + } + + if (!signatureHeader) { + res.status(401).json({ error: "Missing Top.gg Signature" }); + return resolve(false); + } + + const parsedSignature = Object.fromEntries(signatureHeader.split(",").map(part => part.split("="))); + + const timestamp = parsedSignature["t"]; + const signature = parsedSignature[API_VERSION]; + + if (!timestamp || !signature) { + res.status(400).send({ error: "Invalid signature format" }); + return resolve(false); + } + + const hmac = crypto.createHmac("sha256", this.authorization); + const digest = hmac.update(`${timestamp}.${body}`); + + if (signature !== digest) { + res.status(401).json({ error: "Invalid Authorization" }); + return resolve(false); + } try { const parsed = JSON.parse(body.toString("utf8")); From cd679c6df6db68417c25524f8d7a2e879f60fd82 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:58:48 +0700 Subject: [PATCH 44/48] doc: add widgets example to readme --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 5ad9d55..3603d87 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The community-maintained Node.js library for Top.gg. - [Usage](#usage) - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) + - [Generating widget URLs](#generating-widget-urls) - [Webhooks](#webhooks) - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) @@ -117,6 +118,32 @@ await client.postCommands([ ]); ``` +### Generating widget URLs + +#### Large + +```js +const widgetUrl = Topgg.Widget.large(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` + +#### Votes + +```js +const widgetUrl = Topgg.Widget.votes(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` + +#### Owner + +```js +const widgetUrl = Topgg.Widget.owner(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` + +#### Social + +```js +const widgetUrl = Topgg.Widget.social(Topgg.WidgetType.DiscordBot, "574652751745777665"); +``` + ### Webhooks #### Being notified whenever someone voted for your project From 80e77e0c8d05f25467e54eae7ab3096e82d10505 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:12:49 +0700 Subject: [PATCH 45/48] doc: add documentation for getSelf() --- README.md | 31 +++++++++++++++++++++++++++++++ src/structs/Api.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/README.md b/README.md index 3603d87..0908897 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The community-maintained Node.js library for Top.gg. - [Installation](#installation) - [Setting up](#setting-up) - [Usage](#usage) + - [Getting your project's information](#getting-your-projects-information) - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) - [Generating widget URLs](#generating-widget-urls) @@ -40,6 +41,36 @@ const client = new Topgg.Api(process.env.TOPGG_TOKEN); ## Usage +### Getting your project's information + +```js +const project = await client.getSelf(); + +console.log(project); +// => +// { +// id: '218109768489992192', +// name: 'Miki', +// type: 'bot', +// platform: 'discord', +// headline: 'A great bot with tons of features! language | admin | cards | fun | levels | roles | marriage | currency | custom commands!', +// tags: [ +// 'anime', +// 'customizable-behavior', +// 'economy', +// 'fun', +// 'game', +// 'leveling', +// 'multifunctional', +// 'role-management', +// 'roleplay', +// 'social' +// ], +// votes: { current: 1120, total: 313389 }, +// review: { score: 4.38, count: 62245 } +// } +``` + ### Getting your project's vote information of a user #### Discord ID diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 070d658..1bdf089 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -112,6 +112,35 @@ export class Api extends EventEmitter { /** * Gets your project's information. * + * @example + * ```js + * const project = await client.getSelf(); + * + * console.log(project); + * // => + * // { + * // id: '218109768489992192', + * // name: 'Miki', + * // type: 'bot', + * // platform: 'discord', + * // headline: 'A great bot with tons of features! language | admin | cards | fun | levels | roles | marriage | currency | custom commands!', + * // tags: [ + * // 'anime', + * // 'customizable-behavior', + * // 'economy', + * // 'fun', + * // 'game', + * // 'leveling', + * // 'multifunctional', + * // 'role-management', + * // 'roleplay', + * // 'social' + * // ], + * // votes: { current: 1120, total: 313389 }, + * // review: { score: 4.38, count: 62245 } + * // } + * ``` + * * @returns {Promise} Your project's information. */ public async getSelf(): Promise { From 31712b63a64326b43abd428459baa6d2689d6024 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:52:46 +0700 Subject: [PATCH 46/48] refactor: use parsedSignature.t --- src/structs/Webhook.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index c7bc9c1..98bf08a 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -85,17 +85,15 @@ export class Webhook { } const parsedSignature = Object.fromEntries(signatureHeader.split(",").map(part => part.split("="))); - - const timestamp = parsedSignature["t"]; const signature = parsedSignature[API_VERSION]; - if (!timestamp || !signature) { + if (!parsedSignature.t || !signature) { res.status(400).send({ error: "Invalid signature format" }); return resolve(false); } const hmac = crypto.createHmac("sha256", this.authorization); - const digest = hmac.update(`${timestamp}.${body}`); + const digest = hmac.update(`${parsedSignature.t}.${body}`); if (signature !== digest) { res.status(401).json({ error: "Invalid Authorization" }); From 0c1b82b305361c7e35d50451b535c2edb5d62a1d Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:09:29 +0700 Subject: [PATCH 47/48] fix: forgot digest('hex') --- src/structs/Webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index 98bf08a..cd75176 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -93,7 +93,7 @@ export class Webhook { } const hmac = crypto.createHmac("sha256", this.authorization); - const digest = hmac.update(`${parsedSignature.t}.${body}`); + const digest = hmac.update(`${parsedSignature.t}.${body}`).digest("hex"); if (signature !== digest) { res.status(401).json({ error: "Invalid Authorization" }); From 12e2569fc87f53e48bb96095519b441f0953f796 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:10:29 +0700 Subject: [PATCH 48/48] fix: add Bearer prefix to Authorization header --- src/structs/Api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 1bdf089..9699335 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -73,7 +73,7 @@ export class Api extends EventEmitter { body?: Record ): Promise { const headers: IncomingHttpHeaders = {}; - if (this.options.token) headers["authorization"] = this.options.token; + if (this.options.token) headers["authorization"] = `Bearer ${this.options.token}`; if (method !== "GET") headers["content-type"] = "application/json"; let url = BASE_URL + path;