├── .all-contributorsrc
├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── nodejs.yml
│ └── npmpublish.yml
├── .gitignore
├── .jsbeautifyrc
├── .npmrc
├── .nvmrc
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── renovate.json
├── src
├── fallbackListData.ts
├── legacyIdsFallbackData.ts
├── main.ts
└── requests.ts
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "T0TProduction",
10 | "name": "Sebastian Di Luzio",
11 | "avatar_url": "https://avatars.githubusercontent.com/u/18548570?v=4",
12 | "profile": "http://diluz.io",
13 | "contributions": [
14 | "code",
15 | "maintenance",
16 | "doc"
17 | ]
18 | },
19 | {
20 | "login": "advaith1",
21 | "name": "advaith",
22 | "avatar_url": "https://avatars.githubusercontent.com/u/11778454?v=4",
23 | "profile": "https://advaith.io",
24 | "contributions": [
25 | "doc",
26 | "bug"
27 | ]
28 | },
29 | {
30 | "login": "MattIPv4",
31 | "name": "Matt Cowley",
32 | "avatar_url": "https://avatars.githubusercontent.com/u/12371363?v=4",
33 | "profile": "https://mattcowley.co.uk/",
34 | "contributions": [
35 | "doc",
36 | "code"
37 | ]
38 | },
39 | {
40 | "login": "MeerBiene",
41 | "name": "Benedikt Buhles",
42 | "avatar_url": "https://avatars.githubusercontent.com/u/60227302?v=4",
43 | "profile": "https://github.com/MeerBiene",
44 | "contributions": [
45 | "code"
46 | ]
47 | },
48 | {
49 | "login": "promise",
50 | "name": "Glenn",
51 | "avatar_url": "https://avatars.githubusercontent.com/u/10573728?v=4",
52 | "profile": "http://not-tech.support",
53 | "contributions": [
54 | "code",
55 | "bug"
56 | ]
57 | },
58 | {
59 | "login": "jonahsnider",
60 | "name": "Jonah Snider",
61 | "avatar_url": "https://avatars.githubusercontent.com/u/7608555?v=4",
62 | "profile": "https://jonahsnider.com",
63 | "contributions": [
64 | "code"
65 | ]
66 | }
67 | ],
68 | "contributorsPerLine": 7,
69 | "projectName": "BLAPI",
70 | "projectOwner": "botblock",
71 | "repoType": "github",
72 | "repoHost": "https://github.com",
73 | "skipCi": true
74 | }
75 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = false
9 | insert_final_newline = true
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "plugins": ["@typescript-eslint", "import"],
4 | "extends": ["eslint:recommended", "airbnb-base"],
5 | "parser": "@typescript-eslint/parser",
6 | "settings": {
7 | "import/parsers": {
8 | "@typescript-eslint/parser": [".ts", ".tsx"]
9 | }
10 | },
11 | "rules": {
12 | "import/prefer-default-export": "off",
13 | "@typescript-eslint/no-unused-vars": "error",
14 | "no-extra-semi": 0,
15 | "semi": 2,
16 | "indent": ["error", 2],
17 | "quotes": [
18 | "error",
19 | "single",
20 | {
21 | "allowTemplateLiterals": false,
22 | "avoidEscape": true
23 | }
24 | ],
25 | "camelcase": 0,
26 | "no-debugger": 1,
27 | "no-plusplus": 0,
28 | "no-useless-constructor": "off", // TS has some issues with this, so we use their check
29 | "@typescript-eslint/no-useless-constructor": "error",
30 | "import/extensions": 0,
31 | "import/no-unresolved": 0 // TS decides this on its own
32 | },
33 | "ignorePatterns": ["dist/*"]
34 | }
35 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: T0TProduction # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Additional context**
24 | Add any other context about the problem here.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Use Node.js
14 | uses: actions/setup-node@v4
15 | with:
16 | node-version-file: '.nvmrc'
17 | - name: npm install, build, and test
18 | run: |
19 | npm ci
20 | npm run lint
21 | npm run build
22 | npm test --if-present
23 | env:
24 | CI: true
25 |
--------------------------------------------------------------------------------
/.github/workflows/npmpublish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npm
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | publish-npm:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-node@v4
13 | with:
14 | node-version: 18
15 | registry-url: https://registry.npmjs.org/
16 | - run: npm ci
17 | - run: npm run-script build
18 | - run: npm publish
19 | env:
20 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # parcel-bundler cache (https://parceljs.org/)
61 | .cache
62 |
63 | # next.js build output
64 | .next
65 |
66 | # nuxt.js build output
67 | .nuxt
68 |
69 | # vuepress build output
70 | .vuepress/dist
71 |
72 | # Serverless directories
73 | .serverless
74 |
75 | # JetBrains
76 | .idea
77 | testBot/testbot.js
78 |
79 | # Typescript
80 | dist
81 |
--------------------------------------------------------------------------------
/.jsbeautifyrc:
--------------------------------------------------------------------------------
1 | {
2 | "brace_style": "collapse,preserve-inline",
3 | "end_with_newline": true,
4 | "indent_size": 2
5 | }
6 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.16.0
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.eol": "\n",
3 | "explorer.confirmDelete": false,
4 | "git.enableSmartCommit": true,
5 | "window.zoomLevel": 0,
6 | "editor.largeFileOptimizations": false,
7 | "eslint.autoFixOnSave": true,
8 | "javascript.preferences.quoteStyle": "single",
9 | "typescript.preferences.quoteStyle": "single",
10 | "vetur.format.defaultFormatter.js": "prettier-eslint",
11 | "[javascript]": {
12 | "editor.defaultFormatter": "esbenp.prettier-vscode"
13 | },
14 | "javascript.updateImportsOnFileMove.enabled": "always",
15 | "[json]": {
16 | "editor.defaultFormatter": "esbenp.prettier-vscode"
17 | },
18 | "[jsonc]": {
19 | "editor.defaultFormatter": "esbenp.prettier-vscode"
20 | },
21 | "[typescript]": {
22 | "editor.defaultFormatter": "esbenp.prettier-vscode"
23 | },
24 | "git.ignoreLimitWarning": true,
25 | "eslint.validate": [
26 | {
27 | "language": "typescript",
28 | "autoFix": true
29 | },
30 | {
31 | "language": "javascript",
32 | "autoFix": true
33 | }
34 | ],
35 | "editor.formatOnSave": false,
36 | "prettier.singleQuote": true,
37 | "prettier.endOfLine": "lf",
38 | "typescript.tsdk": "node_modules/typescript/lib",
39 | "editor.tabSize": 2,
40 | "editor.codeActionsOnSave": {
41 | "source.fixAll.eslint": "explicit"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at T0TProduction#0001 on Discord. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Sebastian Di Luzio
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BLAPI - the BotListAPI
2 |
3 | [](#contributors-)
4 |
5 |
6 | [](https://nodei.co/npm/blapi/) [](https://packagephobia.now.sh/result?p=blapi)
7 | [](https://www.jsdelivr.com/package/npm/blapi)
8 |
9 | [](https://nodei.co/npm/blapi/)
10 |
11 | BLAPI is a fully typed package to handle posting your discord bot stats to botlists.
12 |
13 | It's intended to be used with discord.js v13 or v14, though you can also manually post your stats.
14 |
15 | BLAPI fully supports external and discord.js internal sharding with and without the use of the [BotBlock API](https://botblock.org/api/docs#count).
16 |
17 | ## Installation
18 |
19 | ### NPM (recommended)
20 |
21 | ```bash
22 | npm i blapi
23 | ```
24 |
25 | ### Yarn
26 |
27 | ```bash
28 | yarn add blapi
29 | ```
30 |
31 | ## Usage
32 |
33 | The list of all supported bot lists and their respective names for the apiKeys object are listed [below](https://github.com/T0TProduction/BLAPI#lists)
34 |
35 | ### Import the lib via ES6 or commonJS modules
36 |
37 | ```js
38 | // ES6
39 | import * as blapi from "blapi";
40 | // or
41 | import { handle } from "blapi"; // Just the functions you want to use
42 | // or commonJS
43 | const blapi = require("blapi");
44 | ```
45 |
46 | ### With discord.js (version 13.x or 14.x)
47 |
48 | ```js
49 | import Discord from "discord.js";
50 |
51 | const bot = new Discord.Client();
52 |
53 | // Post to the APIs every 60 minutes; you can leave out the repeat delay as it defaults to 30
54 | // If the interval is below 3 minutes BLAPI will not use the BotBlock API because of ratelimits
55 | blapi.handle(bot, apiKeys, 60);
56 | ```
57 |
58 | ### Manually, without need of Discord libraries
59 |
60 | ```js
61 | // If you want to post sharddata you can add the optional parameters
62 | // shardID and shardCount should both be integers
63 | // shardsArray should be an integer array containing the guildcounts of the respective shards
64 | blapi.manualPost(guildCount, botID, apiKeys[, shardID, shardCount[, shardsArray]]);
65 | ```
66 |
67 |
68 | ### Logging Options
69 | ```js
70 | // Use this to set the level of logs you're interested in
71 | // By default, warnings and errors will be logged
72 | blapi.setLogging({
73 | logLevel: LogLevel.All
74 | });
75 | ```
76 |
77 | ```js
78 | // If you have your own logger that you want to use pass it to BLAPI like this:
79 | // Important: The logger needs to include the following methods: log.info(), log.warn() and log.error()
80 | blapi.setLogging({
81 | logger: yourCustomLogger
82 | })
83 | ```
84 |
85 | ### Turn off the use of the BotBlock API
86 |
87 | ```js
88 | // Use this to turn off BotBlock usage
89 | // By default it is set to true
90 | blapi.setBotblock(false);
91 | ```
92 |
93 | ### apiKeys
94 |
95 | The JSON object which includes all the API keys should look like this:
96 |
97 | ```json
98 | {
99 | "bot list domain": "API key for that bot list",
100 | "bot list domain": "API key for that bot list"
101 | }
102 | ```
103 |
104 | an example would be:
105 |
106 | ```json
107 | {
108 | "bots.ondiscord.xyz": "dsag38_auth_token_fda6gs",
109 | "discordbots.group": "qos56a_auth_token_gfd8g6"
110 | }
111 | ```
112 |
113 | ## Lists
114 |
115 | BLAPI automatically supports posting to all lists that are [listed on botblock](https://botblock.org/api/docs#lists). For the rare case that their API is down, BLAPI has an [integrated fallback list](https://github.com/botblock/BLAPI/blob/master/src/fallbackListData.ts). This list is kept somewhat up to date inside the repository, but once BLAPI is running in your bot, it will update the internal fallback on a daily basis.
116 |
117 | Supported legacy Ids are supported in a similar fashion. BLAPI supports all [legacy IDs listed on botblock](https://botblock.org/api/docs#legacy-ids). The fallback legacy IDs can be found [here](https://github.com/botblock/BLAPI/blob/master/src/legacyIdsFallbackData.ts), and they are also internally updated on a daily basis once you have BLAPI up and running.
118 |
119 | If at any time you find other bot lists have added an API to post your guildcount, let us know on this repo or by contacting T0TProduction#0001 on Discord. In general, if a list is not listed on BotBlock, the best way to get it added is to directly [join the BotBlock Discord server](https://botblock.org/discord) and request it there.
120 |
121 | ## Development
122 |
123 | To work on BLAPI, install the node version specified in [.nvmrc](https://github.com/botblock/BLAPI/blob/master/.nvmrc).
124 | If you are using nvm on a unix based system, this can be done quickly by using `nvm use` and if the version is not installed, `nvm install`.
125 | Install all the dependencies following the package-lock via `npm ci`.
126 |
127 | This repo enforces eslint rules which are included in the installation.
128 |
129 | ## Contributors ✨
130 |
131 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
132 |
133 |
134 |
135 |
136 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blapi",
3 | "version": "3.1.36",
4 | "license": "MIT",
5 | "repository": "botblock/BLAPI",
6 | "bugs": {
7 | "url": "https://github.com/botblock/BLAPI/issues"
8 | },
9 | "maintainers": [
10 | "Sebastian Di Luzio (https://github.com/T0TProduction)"
11 | ],
12 | "description": "BLAPI is a package to handle posting your discord stats to botlists. It's intended to be used with discord.js, though you can also manually post your stats.",
13 | "homepage": "https://github.com/botblock/BLAPI#readme",
14 | "keywords": [
15 | "discord",
16 | "js",
17 | "discord.js",
18 | "API",
19 | "botlist",
20 | "bot",
21 | "list",
22 | "stats",
23 | "posting",
24 | "automated",
25 | "auto",
26 | "botblock",
27 | "Node.js",
28 | "discordbot",
29 | "discordbots",
30 | "dbl",
31 | "discordbotlist",
32 | "discorbotlists",
33 | "typescript",
34 | "typed"
35 | ],
36 | "main": "dist/main.js",
37 | "types": "dist/main.d.ts",
38 | "scripts": {
39 | "lint": "eslint \"**/*.{ts,js}\"",
40 | "lint:fix": "eslint \"**/*.{ts,js}\" --fix",
41 | "build": "rimraf ./dist && tsc"
42 | },
43 | "files": [
44 | "dist/"
45 | ],
46 | "dependencies": {
47 | "centra": "2.7.0"
48 | },
49 | "peerDependencies": {
50 | "discord.js": "13 - 14"
51 | },
52 | "devDependencies": {
53 | "@types/centra": "2.2.3",
54 | "@types/node": "22.15.29",
55 | "@typescript-eslint/eslint-plugin": "7.18.0",
56 | "@typescript-eslint/parser": "7.18.0",
57 | "eslint-config-airbnb-base": "15.0.0",
58 | "prettier": "3.5.3",
59 | "rimraf": "6.0.1",
60 | "typescript": "5.0.4"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["github>T0TProduction/renovate-config", "default:pinAllExceptPeerDependencies"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/fallbackListData.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'arcane-center.xyz': {
3 | api_docs: 'https://arcane-center.xyz/documentation',
4 | api_post: 'https://arcane-center.xyz/api/:id/stats',
5 | api_field: 'server_count',
6 | api_shard_id: null,
7 | api_shard_count: 'shard_count',
8 | api_shards: null,
9 | api_get: null,
10 | api_all: null,
11 | },
12 | 'bladebotlist.xyz': {
13 | api_docs: null,
14 | api_post: 'https://bladebotlist.xyz/api/bots/:id/stats',
15 | api_field: 'server_count',
16 | api_shard_id: null,
17 | api_shard_count: 'shard_count',
18 | api_shards: null,
19 | api_get: 'https://bladebotlist.xyz/api/bots/:id',
20 | api_all: null,
21 | },
22 | 'blist.xyz': {
23 | api_docs: 'https://blist.xyz/docs/',
24 | api_post: 'https://blist.xyz/api/v2/bot/:id/stats',
25 | api_field: 'server_count',
26 | api_shard_id: null,
27 | api_shard_count: 'shard_count',
28 | api_shards: null,
29 | api_get: 'https://blist.xyz/api/v2/bot/:id',
30 | api_all: null,
31 | },
32 | 'botlists.com': {
33 | api_docs: 'https://botlists.com/api/docs',
34 | api_post: 'https://botlists.com/api/bot',
35 | api_field: 'guild_count',
36 | api_shard_id: null,
37 | api_shard_count: 'shard_count',
38 | api_shards: null,
39 | api_get: 'https://botlists.com/api/bot',
40 | api_all: null,
41 | },
42 | 'botrix.cc': {
43 | api_docs: 'https://docs.botrix.cc/',
44 | api_post: 'https://botrix.cc/api/v1/bot/:id',
45 | api_field: 'servers',
46 | api_shard_id: null,
47 | api_shard_count: 'shards',
48 | api_shards: null,
49 | api_get: 'https://botrix.cc/api/v1/bot/:id',
50 | api_all: null,
51 | },
52 | 'bots.discordlabs.org': {
53 | api_docs: 'https://docs.discordlabs.org/',
54 | api_post: 'https://bots.discordlabs.org/v2/bot/:id/stats',
55 | api_field: 'server_count',
56 | api_shard_id: null,
57 | api_shard_count: 'shard_count',
58 | api_shards: null,
59 | api_get: 'https://bots.discordlabs.org/v2/bot/:id',
60 | api_all: null,
61 | },
62 | 'bots.ondiscord.xyz': {
63 | api_docs: 'https://bots.ondiscord.xyz/info/api',
64 | api_post: 'https://bots.ondiscord.xyz/bot-api/bots/:id/guilds',
65 | api_field: 'guildCount',
66 | api_shard_id: null,
67 | api_shard_count: null,
68 | api_shards: null,
69 | api_get: null,
70 | api_all: null,
71 | },
72 | 'botsdatabase.com': {
73 | api_docs: 'https://docs.botsdatabase.com/',
74 | api_post: 'https://api.botsdatabase.com/v1/bots/:id',
75 | api_field: 'servers',
76 | api_shard_id: null,
77 | api_shard_count: null,
78 | api_shards: 'shards',
79 | api_get: 'https://api.botsdatabase.com/v1/bots/:id',
80 | api_all: null,
81 | },
82 | 'botsfordiscord.com': {
83 | api_docs: 'https://docs.botsfordiscord.com',
84 | api_post: 'https://botsfordiscord.com/api/bot/:id',
85 | api_field: 'server_count',
86 | api_shard_id: null,
87 | api_shard_count: null,
88 | api_shards: null,
89 | api_get: 'https://botsfordiscord.com/api/bot/:id',
90 | api_all: null,
91 | },
92 | 'discord.boats': {
93 | api_docs: 'https://docs.discord.boats/',
94 | api_post: 'https://discord.boats/api/bot/:id',
95 | api_field: 'server_count',
96 | api_shard_id: null,
97 | api_shard_count: null,
98 | api_shards: null,
99 | api_get: 'https://discord.boats/api/bot/:id',
100 | api_all: null,
101 | },
102 | 'discord.bots.gg': {
103 | api_docs: 'https://discord.bots.gg/docs',
104 | api_post: 'https://discord.bots.gg/api/v1/bots/:id/stats',
105 | api_field: 'guildCount',
106 | api_shard_id: 'shardId',
107 | api_shard_count: 'shardCount',
108 | api_shards: null,
109 | api_get: 'https://discord.bots.gg/api/v1/bots/:id',
110 | api_all:
111 | 'https://discord.bots.gg/api/v1/bots?limit=100&sort=guildcount&order=desc',
112 | },
113 | 'discordbotlist.com': {
114 | api_docs: 'https://docs.discordbotlist.com',
115 | api_post: 'https://discordbotlist.com/api/bots/:id/stats',
116 | api_field: 'guilds',
117 | api_shard_id: 'shard_id',
118 | api_shard_count: null,
119 | api_shards: null,
120 | api_get: 'https://discordbotlist.com/api/bots/:id',
121 | api_all: 'https://discordbotlist.com/api/bots',
122 | },
123 | 'discordbots.co': {
124 | api_docs: 'https://discordbots.co/api',
125 | api_post: 'https://api.discordbots.co/v1/public/bot/:id/stats',
126 | api_field: 'serverCount',
127 | api_shard_id: null,
128 | api_shard_count: 'shardCount',
129 | api_shards: null,
130 | api_get: 'https://api.discordbots.co/v1/public/bot/:id',
131 | api_all: null,
132 | },
133 | 'discordextremelist.xyz': {
134 | api_docs: 'https://discordextremelist.xyz/docs#api-routes',
135 | api_post: 'https://api.discordextremelist.xyz/v2/bot/:id/stats',
136 | api_field: 'guildCount',
137 | api_shard_id: null,
138 | api_shard_count: 'shardCount',
139 | api_shards: null,
140 | api_get: 'https://api.discordextremelist.xyz/v2/bot/:id',
141 | api_all: null,
142 | },
143 | 'discordlist.space': {
144 | api_docs: 'https://docs.discordlist.space/',
145 | api_post: 'https://api.discordlist.space/v1/bots/:id',
146 | api_field: 'server_count',
147 | api_shard_id: null,
148 | api_shard_count: null,
149 | api_shards: 'shards',
150 | api_get: 'https://api.discordlist.space/v1/bots/:id',
151 | api_all: 'https://api.discordlist.space/v1/bots',
152 | },
153 | 'discordlistology.com': {
154 | api_docs: 'https://discordlistology.com/developer/documentation',
155 | api_post: 'https://discordlistology.com/api/v1/bots/:id/stats',
156 | api_field: 'servers',
157 | api_shard_id: null,
158 | api_shard_count: 'shards',
159 | api_shards: null,
160 | api_get: 'https://discordlistology.com/api/v1/bots/:id/stats',
161 | api_all: null,
162 | },
163 | 'disforge.com': {
164 | api_docs: 'https://disforge.com/developer',
165 | api_post: 'https://disforge.com/api/botstats/:id',
166 | api_field: 'servers',
167 | api_shard_id: null,
168 | api_shard_count: null,
169 | api_shards: null,
170 | api_get: null,
171 | api_all: null,
172 | },
173 | 'fateslist.xyz': {
174 | api_docs: 'https://fateslist.xyz/api/docs ',
175 | api_post: 'https://fateslist.xyz/api/bots/:id/stats',
176 | api_field: 'guild_count',
177 | api_shard_id: null,
178 | api_shard_count: 'shard_count',
179 | api_shards: null,
180 | api_get: 'https://fateslist.xyz/api/bots/:id',
181 | api_all: null,
182 | },
183 | 'infinitybotlist.com': {
184 | api_docs: 'https://docs.infinitybotlist.com/',
185 | api_post: 'https://api.infinitybotlist.com/bot/:id',
186 | api_field: 'servers',
187 | api_shard_id: null,
188 | api_shard_count: 'shards',
189 | api_shards: null,
190 | api_get: 'https://api.infinitybotlist.com/bot/:id/info',
191 | api_all: null,
192 | },
193 | 'nooder.co': {
194 | api_docs: 'https://nooder.co/api/docs',
195 | api_post: 'https://nooder.co/api/bot/:id/stats',
196 | api_field: 'server_count',
197 | api_shard_id: null,
198 | api_shard_count: null,
199 | api_shards: null,
200 | api_get: 'https://nooder.co/api/bot/:id',
201 | api_all: null,
202 | },
203 | 'paradisebots.net': {
204 | api_docs: 'https://docs.paradisebots.net/',
205 | api_post: 'https://paradisebots.net/api/v1/bot/:id',
206 | api_field: 'server_count',
207 | api_shard_id: null,
208 | api_shard_count: 'shard_count',
209 | api_shards: null,
210 | api_get: 'https://paradisebots.net/api/v1/bots/:id',
211 | api_all: null,
212 | },
213 | 'space-bot-list.xyz': {
214 | api_docs: 'https://spacebots.gitbook.io/tutorial-en/api/stats',
215 | api_post: 'https://space-bot-list.xyz/api/bots/:id',
216 | api_field: 'guilds',
217 | api_shard_id: null,
218 | api_shard_count: null,
219 | api_shards: null,
220 | api_get: 'https://space-bot-list.xyz/api/bots/:id',
221 | api_all: null,
222 | },
223 | 'top.gg': {
224 | api_docs: 'https://top.gg/api/docs',
225 | api_post: 'https://top.gg/api/bots/:id/stats',
226 | api_field: 'server_count',
227 | api_shard_id: 'shard_id',
228 | api_shard_count: 'shard_count',
229 | api_shards: 'shards',
230 | api_get: 'https://top.gg/api/bots/:id',
231 | api_all: null,
232 | },
233 | 'voidbots.net': {
234 | api_docs: 'https://docs.voidbots.net/',
235 | api_post: 'https://api.voidbots.net/bot/stats/:id',
236 | api_field: 'server_count',
237 | api_shard_id: null,
238 | api_shard_count: 'shard_count',
239 | api_shards: null,
240 | api_get: 'https://api.voidbots.net/bot/info/:id',
241 | api_all: null,
242 | },
243 | 'wonderbotlist.com': {
244 | api_docs: 'https://api.wonderbotlist.com/en/',
245 | api_post: 'https://api.wonderbotlist.com/v1/bot/:id',
246 | api_field: 'serveurs',
247 | api_shard_id: null,
248 | api_shard_count: 'shards',
249 | api_shards: null,
250 | api_get: 'https://api.wonderbotlist.com/v1/bot/:id',
251 | api_all: null,
252 | },
253 | 'yabl.xyz': {
254 | api_docs: 'https://yabl.xyz/api',
255 | api_post: 'https://yabl.xyz/api/bot/:id/stats',
256 | api_field: 'guildCount',
257 | api_shard_id: null,
258 | api_shard_count: null,
259 | api_shards: null,
260 | api_get: 'https://yabl.xyz/api/bot/:id',
261 | api_all: 'https://yabl.xyz/api/bots/all',
262 | },
263 | 'topcord.xyz': {
264 | api_docs: 'https://docs.topcord.xyz/#/',
265 | api_post: 'https://api.topcord.xyz/bot/:id/stats',
266 | api_field: 'guilds',
267 | api_shard_id: null,
268 | api_shard_count: 'shards',
269 | api_shards: null,
270 | api_get: 'https://api.topcord.xyz/bot/:id',
271 | api_all: 'https://api.topcord.xyz/bots',
272 | },
273 | };
274 |
--------------------------------------------------------------------------------
/src/legacyIdsFallbackData.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'vultrex.io': 'discordbots.co',
3 | 'space-bot-list.org': 'space-bot-list.xyz',
4 | 'mythicalbots.xyz': 'bots.idledev.org',
5 | 'infinitybots.xyz': 'infinitybotlist.com',
6 | 'infinitybots.com': 'infinitybotlist.com',
7 | 'distop.xyz': 'bots.distop.xyz',
8 | 'discordlist.co': 'nooder.co',
9 | 'discordbots.org': 'top.gg',
10 | 'discordbotreviews.xyz': 'nooder.co',
11 | 'botsparadiscord.xyz': 'botsparadiscord.com',
12 | 'botlist.space': 'discordlist.space',
13 | 'arcane-botcenter.xyz': 'arcane-center.xyz',
14 | };
15 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Response } from 'centra';
2 | import type { Client } from 'discord.js';
3 | import { get, post, UserLogger } from './requests';
4 | import fallbackData from './fallbackListData';
5 | import legacyIdsFallbackData from './legacyIdsFallbackData';
6 |
7 | /* We moved types here to support both TS and JS bots
8 | Importing would make JS fail for modules it can't use
9 | Declaring global fails exporting some types, so TS will fail in that case */
10 | type listDataType = {
11 | [listname: string]: {
12 | api_docs: string | null;
13 | api_post: string | null;
14 | api_field: string | null;
15 | api_shard_id: string | null;
16 | api_shard_count: string | null;
17 | api_shards: string | null;
18 | api_get: string | null;
19 | };
20 | };
21 | type legacyIdDataType = {
22 | [listname: string]: string;
23 | };
24 | type apiKeysObject = { [listname: string]: string };
25 |
26 | // eslint doesnt like enums
27 | // eslint-disable-next-line no-shadow
28 | export enum LogLevel {
29 | None = 0x0000,
30 | ErrorOnly = 0x0010,
31 | WarnAndErrorOnly = 0x0020,
32 | All = 0x0030,
33 | }
34 |
35 | type LogOptions = {
36 | /**
37 | * @deprecated Use logLevel instead.
38 | *
39 | * If you want an equivalent to `extended: true`, use `logLevel: LogLevel.All`
40 | */
41 | extended?: boolean;
42 |
43 | /**
44 | * The log level to use
45 | * @default LogLevel.WarnAndErrorOnly
46 | */
47 | logLevel?: LogLevel;
48 |
49 | /**
50 | * Your custom logger to be used instead of the default console logger
51 | */
52 | logger?: UserLogger;
53 | };
54 |
55 | let listData = fallbackData as listDataType;
56 | let legacyIds = legacyIdsFallbackData as legacyIdDataType;
57 | const lastUpdatedListAt = new Date(1999); // some date that's definitely past
58 | let useBotblockAPI = true;
59 |
60 | let logLevel = LogLevel.WarnAndErrorOnly;
61 |
62 | /**
63 | * the userLogger variable will later be defined with the
64 | * logger supplied by the user if they supplied any
65 | */
66 | // eslint-disable-next-line max-len
67 | let userLogger: UserLogger | undefined;
68 |
69 | function createBlapiMessage(message: string) {
70 | return `BLAPI: ${message}`;
71 | }
72 |
73 | const logger = {
74 | info: (msg: string) => {
75 | if (logLevel < LogLevel.All) {
76 | return;
77 | }
78 | if (userLogger) {
79 | userLogger.info(createBlapiMessage(msg));
80 | } else {
81 | // eslint-disable-next-line no-console
82 | console.info(createBlapiMessage(msg));
83 | }
84 | },
85 | warn: (msg: string) => {
86 | if (logLevel < LogLevel.WarnAndErrorOnly) {
87 | return;
88 | }
89 | if (userLogger) {
90 | userLogger.warn(createBlapiMessage(msg));
91 | } else {
92 | // eslint-disable-next-line no-console
93 | console.warn(createBlapiMessage(msg));
94 | }
95 | },
96 | error: (msg: string) => {
97 | if (logLevel < LogLevel.ErrorOnly) {
98 | return;
99 | }
100 | if (userLogger) {
101 | userLogger.error(createBlapiMessage(msg));
102 | } else {
103 | // eslint-disable-next-line no-console
104 | console.error(createBlapiMessage(msg));
105 | }
106 | },
107 | };
108 |
109 | function convertLegacyIds(apiKeys: apiKeysObject) {
110 | const newApiKeys: apiKeysObject = { ...apiKeys };
111 | Object.entries(legacyIds).forEach(([list, newlist]) => {
112 | if (newApiKeys[list]) {
113 | newApiKeys[newlist] = newApiKeys[list];
114 | delete newApiKeys[list];
115 | }
116 | });
117 | return newApiKeys;
118 | }
119 |
120 | function buildBotblockData(
121 | apiKeys: apiKeysObject,
122 | bot_id: string,
123 | server_count: number,
124 | shard_id?: number,
125 | shard_count?: number,
126 | shards?: Array,
127 | ) {
128 | return {
129 | ...convertLegacyIds(apiKeys),
130 | bot_id,
131 | server_count,
132 | shard_id,
133 | shard_count,
134 | shards,
135 | };
136 | }
137 |
138 | /**
139 | * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.} ;
140 | * it also includes other metadata including sharddata
141 | */
142 | async function postToAllLists(
143 | apiKeys: apiKeysObject,
144 | client_id: string,
145 | server_count: number,
146 | shard_id?: number,
147 | shard_count?: number,
148 | shards?: Array,
149 | ): Promise> {
150 | // make sure we have all lists we can post to and their apis
151 | const currentDate = new Date();
152 | if (!listData || lastUpdatedListAt < currentDate) {
153 | // we try to update the listdata every day
154 | // in case new lists are added but the code is not restarted
155 | lastUpdatedListAt.setDate(currentDate.getDate() + 1);
156 | try {
157 | const tmpListData = await get(
158 | 'https://botblock.org/api/lists?filter=true',
159 | logger,
160 | );
161 | // make sure we only save it if nothing goes wrong
162 | if (tmpListData) {
163 | listData = tmpListData;
164 | logger.info('Updated list endpoints.');
165 | } else {
166 | logger.error('Got empty list of endpoints from botblock.');
167 | }
168 | } catch (e) {
169 | logger.error(String(e));
170 | logger.error(
171 | "Something went wrong when contacting BotBlock for the API of the lists, so we're using an older preset. Some lists might not be available because of this.",
172 | );
173 | }
174 | try {
175 | const tmpLegacyIdsData = await get(
176 | 'https://botblock.org/api/legacy-ids',
177 | logger,
178 | );
179 | // make sure we only save it if nothing goes wrong
180 | if (tmpLegacyIdsData) {
181 | legacyIds = tmpLegacyIdsData;
182 | logger.info('Updated legacy Ids.');
183 | } else {
184 | logger.error('Got empty list of legacy Ids from botblock.');
185 | }
186 | } catch (e) {
187 | logger.error(String(e));
188 | logger.error(
189 | "Something went wrong when contacting BotBlock for legacy Ids, so we're using an older preset. Some lists might not be available because of this.",
190 | );
191 | }
192 | }
193 |
194 | const posts: Array> = [];
195 |
196 | const updatedApiKeys = convertLegacyIds(apiKeys);
197 |
198 | Object.entries(listData).forEach(([listname]) => {
199 | if (updatedApiKeys[listname] && listData[listname].api_post) {
200 | const list = listData[listname];
201 | if (!list.api_post || !list.api_field) {
202 | return;
203 | }
204 | const apiPath = list.api_post.replace(':id', client_id);
205 | const sendObj: { [key: string]: any } = {};
206 | sendObj[list.api_field] = server_count;
207 | if (shard_id && list.api_shard_id) {
208 | sendObj[list.api_shard_id] = shard_id;
209 | }
210 | if (shard_count && list.api_shard_count) {
211 | sendObj[list.api_shard_count] = shard_count;
212 | }
213 | if (shards && list.api_shards) {
214 | sendObj[list.api_shards] = shards;
215 | }
216 |
217 | posts.push(post(apiPath, updatedApiKeys[listname], sendObj, logger));
218 | }
219 | });
220 |
221 | return Promise.all(posts);
222 | }
223 |
224 | async function runHandleInternalInSeconds(
225 | client: Client,
226 | apiKeys: apiKeysObject,
227 | repeatInterval: number,
228 | seconds: number,
229 | ): Promise {
230 | setTimeout(
231 | /* eslint-disable-next-line no-use-before-define */
232 | () => handleInternal(client, apiKeys, repeatInterval),
233 | 1000 * seconds,
234 | );
235 | }
236 |
237 | /**
238 | * @param client Discord.js client
239 | * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.}
240 | * @param repeatInterval Number of minutes between each repetition
241 | */
242 | async function handleInternal(
243 | client: Client,
244 | apiKeys: apiKeysObject,
245 | repeatInterval: number,
246 | isFirstRun = false,
247 | ): Promise {
248 | // call this function again in the next interval
249 | runHandleInternalInSeconds(client, apiKeys, repeatInterval, 60 * repeatInterval);
250 |
251 | if (client.user) {
252 | const client_id = client.user.id;
253 | let unchanged;
254 | let shard_count: number | undefined;
255 | let shards: Array | undefined;
256 | let server_count = 0;
257 | let shard_id: number | undefined;
258 | // Checks if bot is sharded
259 | // Only run posting from shard 0
260 | if (client.shard?.ids.includes(0)) {
261 | shard_count = client.shard.count;
262 | shard_id = client.shard.ids.at(0); // this should always only be a single number
263 |
264 | // This will get as much info as it can, without erroring
265 | try {
266 | const guildSizes: Array = await client.shard.broadcastEval(
267 | (broadcastedClient) => broadcastedClient.guilds.cache.size,
268 | );
269 | const shardCounts = guildSizes.filter((count: number) => count !== 0);
270 | if (shardCounts.length !== client.shard.count) {
271 | // If not all shards are up yet, we skip this run of handleInternal
272 | return;
273 | }
274 | server_count = shardCounts.reduce(
275 | (prev: number, val: number) => prev + val,
276 | 0,
277 | );
278 | } catch (e) {
279 | logger.error(String(e));
280 | logger.error('Error while fetching shard server counts:');
281 | }
282 | // Checks if bot is sharded with internal sharding
283 | } else if (client.ws.shards.size > 1) {
284 | shard_count = client.ws.shards.size;
285 | // Get array of shards
286 | shards = client.ws.shards.map(
287 | (shard) => client.guilds.cache.filter((guild) => guild.shardId === shard.id).size,
288 | );
289 |
290 | if (shards.length !== client.ws.shards.size) {
291 | // If not all shards are up yet, we skip this run of handleInternal and try again later
292 | if (isFirstRun) {
293 | const secondsToWait = 10;
294 | logger.info(`Not all shards are up yet, so we're trying again in ${secondsToWait} seconds.`);
295 | runHandleInternalInSeconds(client, apiKeys, repeatInterval, secondsToWait);
296 | return;
297 | }
298 | logger.error('Not all shards are up yet, but this is not the first time we\'re trying so we will wait for the entire interval.');
299 | return;
300 | }
301 | server_count = shards.reduce(
302 | (prev: number, val: number) => prev + val,
303 | 0,
304 | );
305 | // Check if bot is not sharded at all, but still wants to send server count
306 | // (it's recommended to shard your bot, even if it's only one shard)
307 | } else if (!client.shard) {
308 | server_count = client.guilds.cache.size;
309 | } else {
310 | unchanged = true;
311 | } // nothing has changed, therefore we don't send any data
312 | if (!unchanged) {
313 | if (repeatInterval > 2 && useBotblockAPI) {
314 | // if the interval isnt below the BotBlock ratelimit, use their API
315 | await post(
316 | 'https://botblock.org/api/count',
317 | 'no key needed for this',
318 | buildBotblockData(
319 | apiKeys,
320 | client_id,
321 | server_count,
322 | shard_id,
323 | shard_count,
324 | shards,
325 | ),
326 | logger,
327 | );
328 |
329 | // they blacklisted botblock, so we need to do this, posting their stats manually
330 | if (apiKeys['top.gg']) {
331 | await postToAllLists(
332 | { 'top.gg': apiKeys['top.gg'] },
333 | client_id,
334 | server_count,
335 | shard_id,
336 | shard_count,
337 | shards,
338 | );
339 | }
340 | } else {
341 | await postToAllLists(
342 | apiKeys,
343 | client_id,
344 | server_count,
345 | shard_id,
346 | shard_count,
347 | shards,
348 | );
349 | }
350 | }
351 | } else {
352 | if (isFirstRun) {
353 | const secondsToWait = 10;
354 | logger.info(`Discord client seems to not be connected yet, so we're trying again in ${secondsToWait} seconds.`);
355 | runHandleInternalInSeconds(client, apiKeys, repeatInterval, secondsToWait);
356 | return;
357 | }
358 | logger.error('Discord client seems to not be connected yet, but this is not the first time we\'re trying so we will wait for the entire interval.');
359 | }
360 | }
361 |
362 | /**
363 | * This function is for automated use with discord.js
364 | * @param discordClient Client via wich your code is connected to Discord
365 | * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.}
366 | * @param repeatInterval Number of minutes until you want to post again, leave out to use 30
367 | */
368 | export function handle(
369 | discordClient: Client,
370 | apiKeys: apiKeysObject,
371 | repeatInterval?: number,
372 | ): Promise {
373 | return handleInternal(
374 | discordClient,
375 | apiKeys,
376 | !repeatInterval || repeatInterval < 1 ? 30 : repeatInterval,
377 | true,
378 | );
379 | }
380 |
381 | /**
382 | * For when you don't use discord.js or just want to post to manual times
383 | * @param guildCount Integer value of guilds your bot is serving
384 | * @param botId Snowflake of the ID the user your bot is using
385 | * @param apiKeys A JSON object formatted like: {"botlist name":"API Keys for that list", etc.}
386 | * @param shardId (optional) The shard ID, which will be used to identify the
387 | * shards valid for posting
388 | * (and for super efficient posting with BLAPIs own distributer when not using botBlock)
389 | * @param shardCount (optional) The number of shards the bot has, which is posted to the lists
390 | * @param shards (optional) An array of guild counts of each single shard
391 | * (this should be a complete list, and only a single shard will post it)
392 | */
393 | export async function manualPost(
394 | guildCount: number,
395 | botID: string,
396 | apiKeys: apiKeysObject,
397 | shard_id?: number,
398 | shard_count?: number,
399 | shards?: Array,
400 | ): Promise> {
401 | const updatedApiKeys = convertLegacyIds(apiKeys);
402 | const client_id = botID;
403 | let server_count = guildCount;
404 | // check if we want to use sharding
405 | if (shard_id === 0 || (shard_id && !shards)) {
406 | // if we don't have all the shard info in one place well try to post every shard itself
407 | if (shards) {
408 | if (shards.length !== shard_count) {
409 | throw new Error(
410 | `BLAPI: Shardcount (${shard_count}) does not equal the length of the shards array (${shards.length}).`,
411 | );
412 | }
413 | server_count = shards.reduce(
414 | (prev: number, val: number) => prev + val,
415 | 0,
416 | );
417 | }
418 | }
419 | const responses: Array = [];
420 |
421 | if (useBotblockAPI) {
422 | responses.push(
423 | await post(
424 | 'https://botblock.org/api/count',
425 | 'no key needed for this',
426 | buildBotblockData(
427 | updatedApiKeys,
428 | client_id,
429 | server_count,
430 | shard_id,
431 | shard_count,
432 | shards,
433 | ),
434 | logger,
435 | ),
436 | );
437 | if (updatedApiKeys['top.gg']) {
438 | responses.concat(
439 | await postToAllLists(
440 | { 'top.gg': updatedApiKeys['top.gg'] },
441 | client_id,
442 | server_count,
443 | shard_id,
444 | shard_count,
445 | shards,
446 | ),
447 | );
448 | }
449 | } else {
450 | responses.concat(
451 | await postToAllLists(
452 | updatedApiKeys,
453 | client_id,
454 | server_count,
455 | shard_id,
456 | shard_count,
457 | shards,
458 | ),
459 | );
460 | }
461 | return responses;
462 | }
463 |
464 | export function setLogging(logOptions: LogOptions): void {
465 | if (
466 | typeof logOptions.logLevel === 'number'
467 | ) {
468 | logLevel = logOptions.logLevel;
469 | } else if (
470 | // backwards compatibility
471 | typeof logOptions.extended === 'boolean'
472 | ) {
473 | logLevel = logOptions.extended ? LogLevel.All : LogLevel.WarnAndErrorOnly;
474 | }
475 | // no logger supplied by user
476 | if (!Object.prototype.hasOwnProperty.call(logOptions, 'logger')) {
477 | return;
478 | }
479 | const passedLogger = logOptions.logger!; // we checked that it exists beforehand
480 | // making sure the logger supplied by the user has our required log levels (info, warn, error)
481 | if (
482 | typeof passedLogger.info !== 'function'
483 | || typeof passedLogger.warn !== 'function'
484 | || typeof passedLogger.error !== 'function'
485 | ) {
486 | throw new Error(
487 | 'Your supplied logger does not seem to expose the log levels BLAPI needs to work. Make sure your logger offers the following methods: info() warn() error()',
488 | );
489 | }
490 | userLogger = passedLogger;
491 | }
492 |
493 | export function setBotblock(useBotblock: boolean): void {
494 | useBotblockAPI = useBotblock;
495 | }
496 |
--------------------------------------------------------------------------------
/src/requests.ts:
--------------------------------------------------------------------------------
1 | import c from 'centra';
2 |
3 | export type UserLogger = {
4 | info: (msg: string) => void;
5 | warn: (msg: string) => void;
6 | error: (msg: string) => void;
7 | };
8 |
9 | /** Custom post function based on centra */
10 | export async function post(
11 | apiPath: string,
12 | apiKey: string,
13 | sendObj: object,
14 | logger: UserLogger,
15 | ) {
16 | const postData = JSON.stringify(sendObj);
17 | try {
18 | const request = c(apiPath, 'POST');
19 | request.reqHeaders = {
20 | 'Content-Type': 'application/json',
21 | 'Content-Length': String(postData.length),
22 | Authorization: apiKey,
23 | };
24 | const response = await request.body(postData).send();
25 |
26 | logger.info(` posted to ${apiPath}`);
27 | logger.info(` statusCode: ${response.statusCode}`);
28 | logger.info(` headers: ${JSON.stringify(response.headers)}`);
29 | // it's text because text accepts both json and plain text, while json only supports json
30 | logger.info(` data: ${await response.text()}`);
31 |
32 | return response;
33 | } catch (e) {
34 | const error = e as Error;
35 | logger.error(error.message);
36 | return { error };
37 | }
38 | }
39 | /** Custom get function based on centra */
40 | export async function get(url: string, logger: UserLogger): Promise {
41 | try {
42 | const response = await c(url, 'GET').send();
43 | return response.json();
44 | } catch (e) {
45 | const error = e as Error;
46 | logger.error(error.message);
47 | throw new Error(`Request to ${url} failed with Errorcode ${error.name}`);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules", "dist"],
3 | "include": ["**/*.ts", "**/*.js"],
4 | "compilerOptions": {
5 | "target": "esnext",
6 | "module": "commonjs",
7 | "lib": ["esnext", "esnext.asynciterable", "dom"],
8 | "declaration": true,
9 | "outDir": "dist",
10 | "strict": true,
11 | "esModuleInterop": true,
12 | "moduleResolution": "node",
13 | "sourceMap": true,
14 | "strictNullChecks": true,
15 | "typeRoots": ["types", "node_modules/@types"],
16 | "types": ["@types/node"],
17 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
18 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
19 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
20 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */
21 | }
22 | }
23 |
--------------------------------------------------------------------------------