├── .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 | [![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | [![npm downloads](https://img.shields.io/npm/dt/blapi.svg)](https://nodei.co/npm/blapi/) [![install size](https://packagephobia.now.sh/badge?p=blapi)](https://packagephobia.now.sh/result?p=blapi) 7 | [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/blapi/badge?style=rounded)](https://www.jsdelivr.com/package/npm/blapi) 8 | 9 | [![nodei](https://nodei.co/npm/blapi.png)](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 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |

Sebastian Di Luzio

💻 🚧 📖

advaith

📖 🐛

Matt Cowley

📖 💻

Benedikt Buhles

💻

Glenn

💻 🐛

Jonah Snider

💻
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 | --------------------------------------------------------------------------------