├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── README.md ├── TestPlan.md ├── config.example.json ├── helpers.js ├── package-lock.json ├── package.json ├── register.js ├── registerGlobal.js ├── run.js ├── runReverse.js ├── syncBotLogo.png └── syncBotLogoBlack.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # 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: jonathonor 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 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Contributors][contributors-shield]][contributors-url] 3 | [![Forks][forks-shield]][forks-url] 4 | [![Stargazers][stars-shield]][stars-url] 5 | [![Issues][issues-shield]][issues-url] 6 | 7 | 8 |
9 |
10 | 11 | Logo 12 | 13 | 14 |

SyncBot

15 |

16 | A bot that syncs roles between one main server, and multiple other discord servers.

17 |

18 |

OR

19 | A bot that sync roles between multiple synced servers, and one main server.

20 | Explore the docs » 21 |
22 |
23 | My Discord 24 | · 25 | Report Bug 26 | · 27 | Request Feature 28 | · 29 | My Website 30 |

31 |
32 | 33 |
34 | Buy Me a Coffee at ko-fi.com 35 |
36 | 37 | 38 | ## About The Project 39 | 40 | This is a side project I started back in 2019 to help a friend in managing his multiple discord servers. The bot is very helpful for server owners who share roles between servers based on certain subscriptions or packages the user has gained access to. I would highly recommend checking out my website where I dive in deep to how the bot works, what all it does, and even share some walkthrough videos of it in action. Thanks for stopping by and reach out to me on Discord if youd like to connect! 41 | 42 |

(back to top)

43 | 44 | ## Use Cases - Running Regular SyncBot 45 | #### (a single main server feeding roles to multiple synced servers) 46 | Manual Operations 47 | - Adding Roles 48 | - You have a user in your main server, and you use the discord UI to give the user role1, the bot will look up any role named "role1" in each additional server and give the user that role in each of the additional servers as well. 49 | - You have a user in your main server, and you use the /add command with @role1 @username, the bot will look up the role named "role1" in each additional server and give the user that role in each of the additional servers as well. 50 | - Removing Roles 51 | - You have a user in your main server, and you use the discord UI to remove role1 from the user, the bot will see if the user exists in each of the additional servers, and will remove the role from them there as well. 52 | - You have a user in your main server, and you use the /remove command with @role1 @username, the bot will see if the user exists in each of the additinoal servers, and will remove the role from them there as well. 53 | - Role Verification 54 | - Your bot has gone down at some point and you don't know what roles each user has in your synced servers. 55 | - You can run the role-checker command with the analyze option which sends you a file detailing the differences between your users roles in each server. 56 | - You can run the role-checker command with the force-sync option which will return all users in all synced servers roles to match the main server roles they have. 57 | 58 | Automatic Operations 59 | - You have a user in your main server, and you invite them to an additional server. When the user joins the additional server, any roles that they have in the main server will be applied automatically to them on join of the additional server. 60 | - example: Jim is part of the mainserver and has role1, and then Jim joins a synced server. Jim automatically has role1 upon joining the synced server. 61 | - You have a user in your main server, and you remove them from the server, or they leave the main server. All roles that the user has in the main server are removed from the user in all additional synced servers. 62 | - example: Jim is part of the mainserver and has role1, and role2, when Jim is kicked, or leaves the mainserver, but stays in any additional servers, he will no longer have role1 or role2 in any additional server. He also will not have role1 or role2 upon rejoining the mainserver until they are given back to him. 63 | 64 | ## Use Cases - Running Reverse SyncBot 65 | #### (many synced servers feeding roles back to a single main server) 66 | Manual Operations 67 | - Adding Roles 68 | - You have a user in a synced server, and you use the discord UI to give the user role1, the bot will look up any role named "role1" in the main server and give the user that role in the main server. 69 | - You have a user in a synced server, and you use the /add command with @role1 @username, the bot will look up the role named "role1" in the main server and give the user that role in the main server. 70 | 71 | - Removing Roles 72 | - You have a user in a synced server, and you use the discord UI to remove role1 from the user, the bot will see if the user exists in the main server, and will remove the role from them there as well. 73 | - You have a user in a synced server, and you use the /remove command with @role1 @username, the bot will see if the user exists inin the main server, and will remove the role from them there as well. 74 | - Role Verification 75 | - Your bot has gone down at some point and you don't know what roles each user has in your synced servers. 76 | - You can run the role-checker command with the analyze option which sends you a file detailing the differences between your users roles in each server. 77 | - You can run the role-checker command with the force-sync option which will return all users in all synced servers roles to match the main server roles they have. 78 | 79 | Automatic Operations 80 | - You have a user in a synced server, and you invite them to the main server. When the user joins the main server, any roles that they have in a synced server will be applied automatically to them on join of the main server. 81 | - example: Jim is part of a synced server and has role1, and then Jim joins the main server. Jim automatically has role1 upon joining the main server. 82 | - You have a user a synced server, and you remove them from the synced server, or they leave the synced server. All roles that the user has in the synced server are removed from the user in the main server. 83 | - example: Jim is part of a synced server and has role1, and role2 there, when Jim is kicked, or leaves that synced server, but stays in the main server, he will no longer have role1 or role2 in the main server. He also will not have role1 or role2 upon rejoining the mainserver until they are given back to him in a synced server. 84 | 85 | ## Installing 86 | - requirements : 87 | - node v16.11.1 88 | - discord.js v13.2.0 89 | - example install : 90 | - cd /Documents 91 | - git clone https://github.com/jonathonor/syncBot.git 92 | - cd syncBot 93 | - npm install discord.js @discordjs/rest discord-api-types axios 94 | - follow the config steps at [SyncBot Config Documentation](https://jonsbots.com/syncbot/#aioseo-explain-config-file) to populate the config.json file before executing the next two commands 95 | 96 | - To start regular sync bot 97 | - node register.js (this registers the /add /remove and /role-checker slash commands for your main server) 98 | - node run.js 99 | 100 | - To start regular sync bot 101 | - node registerGlobal.js (this registers the /add /remove and /role-checker slash commands for all your servers) 102 | - node runReverse.js 103 | 104 | ## Contributing 105 | 106 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 107 | 108 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 109 | Don't forget to give the project a star! Thanks again! 110 | 111 | 1. Fork the Project 112 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 113 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 114 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 115 | 5. Open a Pull Request 116 | 117 |

(back to top)

118 | 119 | 120 | 121 | [contributors-shield]: https://img.shields.io/github/contributors/jonathonor/syncbot.svg?style=for-the-badge 122 | [contributors-url]: https://github.com/jonathonor/syncbot/graphs/contributors 123 | [forks-shield]: https://img.shields.io/github/forks/jonathonor/syncbot.svg?style=for-the-badge 124 | [forks-url]: https://github.com/jonathonor/syncbot/network/members 125 | [stars-shield]: https://img.shields.io/github/stars/jonathonor/syncbot.svg?style=for-the-badge 126 | [stars-url]: https://github.com/jonathonor/syncbot/stargazers 127 | [issues-shield]: https://img.shields.io/github/issues/jonathonor/syncbot.svg?style=for-the-badge 128 | [issues-url]: https://github.com/jonathonor/syncbot/issues 129 | [license-shield]: https://img.shields.io/github/license/jonathonor/syncbot.svg?style=for-the-badge 130 | [license-url]: https://github.com/jonathonor/syncbot/blob/master/LICENSE.txt 131 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 132 | [linkedin-url]: https://linkedin.com/in/linkedin_username 133 | [product-screenshot]: images/screenshot.png 134 | [Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white 135 | [Next-url]: https://nextjs.org/ 136 | [React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB 137 | [React-url]: https://reactjs.org/ 138 | [Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D 139 | [Vue-url]: https://vuejs.org/ 140 | [Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white 141 | [Angular-url]: https://angular.io/ 142 | [Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 143 | [Svelte-url]: https://svelte.dev/ 144 | [Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white 145 | [Laravel-url]: https://laravel.com 146 | [Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white 147 | [Bootstrap-url]: https://getbootstrap.com 148 | [JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white 149 | [JQuery-url]: https://jquery.com 150 | -------------------------------------------------------------------------------- /TestPlan.md: -------------------------------------------------------------------------------- 1 | 1. When user joins main server - do nothing. 2 | 2. When user joins synced server - look up roles in main server and apply in synced server 3 | 3. When user leaves main server - remove roles in main server from synced servers 4 | 4. When user leaves synced server - do nothing 5 | 5. When role is added in main server - add role in synced servers 6 | 6. When role is removed from main server - remove role in synced servers 7 | 8 | ToDo Tasks: 9 | 1. Move permissions checks to application commands. 10 | 2. Make role-checker more performant. 11 | 12 | RoleAnalyze 13 | 1. If user has role in main server but not in synced server, add it in synced server 14 | 2. If user has role in synced server, but not in main server, remove it in synced server 15 | 3. If user does not exist in in main server, remove all roles that exist in main server -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "BOT TOKEN HERE", 3 | "applicationId": "BOT CLIENT ID HERE, also called application id", 4 | "mainServer": "INSERT MAIN SERVER ID HERE", 5 | "syncedServers": ["INSERT SYNCED SERVER ID HERE", "INSERT ADDITIONAL SYNCED SERVER ID HERE (delete if only syncing one server)"], 6 | "allowedRoleId": "Role ID in main server of users who you want to let add/remove role", 7 | "allowedRoleName": "***Used for runReverse only, input the ROLE NAME of users who you want to let add/remove role", 8 | "logChannelId": "INSERT LOG CHANNEL ID HERE (must be in server 1)" 9 | } -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {the interaction from discord command message} interaction 4 | * @param {the function to call with each member} action 5 | * @param {the function to execute after all members have been processed with the action} callback 6 | * @param {any options that need to be passed through to the action or callback} options 7 | */ 8 | export let iterateThroughMembers = async (interaction, action, callback, options) => { 9 | let data = { membersAnalyzed: 0, membersWithDifferences: [], errors: [] }; 10 | interaction.guild.members 11 | .fetch() 12 | .then(async (members) => { 13 | for (const member of members.values()) { 14 | if (member.manageable && !member.user.bot) { 15 | data = await action(member, interaction, data, options); 16 | } else { 17 | let error = `Unable to apply action: ${action.name} to ${member.user.username}`; 18 | 19 | if (!member.manageable) { 20 | error += ". Member is not manageable." 21 | } 22 | 23 | if (member.user.bot) { 24 | error += ". Member is a bot." 25 | } 26 | 27 | console.log(error); 28 | data.errors.push(error); 29 | } 30 | } 31 | callback(interaction, data, options); 32 | }) 33 | .catch(console.log); 34 | }; 35 | 36 | export let colorLog = (color, message) => { 37 | //colors 38 | // \x1b[91m Red 39 | // \x1b[93m Yellow 40 | // \x1b[94m Blue 41 | // \x1b[0m back to console default 42 | switch (color) { 43 | case 'error': 44 | console.log(`\x1b[91m ${message} \x1b[0m`) 45 | case 'warning': 46 | console.log(`\x1b[93m ${message} \x1b[0m`) 47 | case 'info': 48 | console.log(`\x1b[94m ${message} \x1b[0m`) 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syncbot", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "syncbot", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@discordjs/rest": "^1.0.0", 13 | "axios": "^0.27.2", 14 | "discord-api-types": "^0.36.2", 15 | "discord.js": "^14.0.3", 16 | "lowdb": "^3.0.0" 17 | }, 18 | "engines": { 19 | "node": "16.x" 20 | } 21 | }, 22 | "node_modules/@discordjs/builders": { 23 | "version": "1.0.0", 24 | "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.0.0.tgz", 25 | "integrity": "sha512-8y91ZfpOHubiGJu5tVyGI9tQCEyHZDTeqUWVcJd0dq7B96xIf84S0L4fwmD1k9zTe1eqEFSk0gc7BpY+FKn7Ww==", 26 | "dependencies": { 27 | "@sapphire/shapeshift": "^3.5.1", 28 | "discord-api-types": "^0.36.2", 29 | "fast-deep-equal": "^3.1.3", 30 | "ts-mixer": "^6.0.1", 31 | "tslib": "^2.4.0" 32 | }, 33 | "engines": { 34 | "node": ">=16.9.0" 35 | } 36 | }, 37 | "node_modules/@discordjs/collection": { 38 | "version": "1.0.0", 39 | "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.0.0.tgz", 40 | "integrity": "sha512-nAxDQYE5dNAzEGQ7HU20sujDsG5vLowUKCEqZkKUIlrXERZFTt/60zKUj/g4+AVCGeq+pXC5hivMaNtiC+PY5Q==", 41 | "engines": { 42 | "node": ">=16.9.0" 43 | } 44 | }, 45 | "node_modules/@discordjs/rest": { 46 | "version": "1.0.0", 47 | "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.0.0.tgz", 48 | "integrity": "sha512-uDAvnE0P2a8axMdD4C51EGjvCRQ2HZk2Yxf6vHWZgIqG87D8DGKMPwmquIxrrB07MjV+rwci2ObU+mGhGP+bJg==", 49 | "dependencies": { 50 | "@discordjs/collection": "^1.0.0", 51 | "@sapphire/async-queue": "^1.3.2", 52 | "@sapphire/snowflake": "^3.2.2", 53 | "discord-api-types": "^0.36.2", 54 | "file-type": "^17.1.2", 55 | "tslib": "^2.4.0", 56 | "undici": "^5.7.0" 57 | }, 58 | "engines": { 59 | "node": ">=16.9.0" 60 | } 61 | }, 62 | "node_modules/@sapphire/async-queue": { 63 | "version": "1.3.2", 64 | "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.2.tgz", 65 | "integrity": "sha512-rUpMLATsoAMnlN3gecAcr9Ecnw1vG7zi5Xr+IX22YzRzi1k9PF9vKzoT8RuEJbiIszjcimu3rveqUnvwDopz8g==", 66 | "engines": { 67 | "node": ">=v14.0.0", 68 | "npm": ">=7.0.0" 69 | } 70 | }, 71 | "node_modules/@sapphire/shapeshift": { 72 | "version": "3.5.1", 73 | "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.5.1.tgz", 74 | "integrity": "sha512-7JFsW5IglyOIUQI1eE0g6h06D/Far6HqpcowRScgCiLSqTf3hhkPWCWotVTtVycnDCMYIwPeaw6IEPBomKC8pA==", 75 | "dependencies": { 76 | "fast-deep-equal": "^3.1.3", 77 | "lodash.uniqwith": "^4.5.0" 78 | }, 79 | "engines": { 80 | "node": ">=v14.0.0", 81 | "npm": ">=7.0.0" 82 | } 83 | }, 84 | "node_modules/@sapphire/snowflake": { 85 | "version": "3.2.2", 86 | "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.2.tgz", 87 | "integrity": "sha512-ula2O0kpSZtX9rKXNeQMrHwNd7E4jPDJYUXmEGTFdMRfyfMw+FPyh04oKMjAiDuOi64bYgVkOV3MjK+loImFhQ==", 88 | "engines": { 89 | "node": ">=v14.0.0", 90 | "npm": ">=7.0.0" 91 | } 92 | }, 93 | "node_modules/@tokenizer/token": { 94 | "version": "0.3.0", 95 | "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", 96 | "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" 97 | }, 98 | "node_modules/@types/node": { 99 | "version": "18.0.6", 100 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.6.tgz", 101 | "integrity": "sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==" 102 | }, 103 | "node_modules/@types/ws": { 104 | "version": "8.5.3", 105 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", 106 | "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", 107 | "dependencies": { 108 | "@types/node": "*" 109 | } 110 | }, 111 | "node_modules/asynckit": { 112 | "version": "0.4.0", 113 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 114 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 115 | }, 116 | "node_modules/axios": { 117 | "version": "0.27.2", 118 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", 119 | "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", 120 | "dependencies": { 121 | "follow-redirects": "^1.14.9", 122 | "form-data": "^4.0.0" 123 | } 124 | }, 125 | "node_modules/busboy": { 126 | "version": "1.6.0", 127 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 128 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 129 | "dependencies": { 130 | "streamsearch": "^1.1.0" 131 | }, 132 | "engines": { 133 | "node": ">=10.16.0" 134 | } 135 | }, 136 | "node_modules/combined-stream": { 137 | "version": "1.0.8", 138 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 139 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 140 | "dependencies": { 141 | "delayed-stream": "~1.0.0" 142 | }, 143 | "engines": { 144 | "node": ">= 0.8" 145 | } 146 | }, 147 | "node_modules/delayed-stream": { 148 | "version": "1.0.0", 149 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 150 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 151 | "engines": { 152 | "node": ">=0.4.0" 153 | } 154 | }, 155 | "node_modules/discord-api-types": { 156 | "version": "0.36.2", 157 | "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.2.tgz", 158 | "integrity": "sha512-TunPAvzwneK/m5fr4hxH3bMsrtI22nr9yjfHyo5NBGMjpsAauGNiGCmwoFf0oO3jSd2mZiKUvZwCKDaB166u2Q==" 159 | }, 160 | "node_modules/discord.js": { 161 | "version": "14.0.3", 162 | "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.0.3.tgz", 163 | "integrity": "sha512-wH/VQl4CqN8/+dcXEtYis1iurqxGlDpEe0O4CqH5FGqZGIjVpTdtK0STXXx7bVNX8MT/0GvLZLkmO/5gLDWZVg==", 164 | "dependencies": { 165 | "@discordjs/builders": "^1.0.0", 166 | "@discordjs/collection": "^1.0.0", 167 | "@discordjs/rest": "^1.0.0", 168 | "@sapphire/snowflake": "^3.2.2", 169 | "@types/ws": "^8.5.3", 170 | "discord-api-types": "^0.36.2", 171 | "fast-deep-equal": "^3.1.3", 172 | "lodash.snakecase": "^4.1.1", 173 | "tslib": "^2.4.0", 174 | "undici": "^5.8.0", 175 | "ws": "^8.8.1" 176 | }, 177 | "engines": { 178 | "node": ">=16.9.0" 179 | } 180 | }, 181 | "node_modules/fast-deep-equal": { 182 | "version": "3.1.3", 183 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 184 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 185 | }, 186 | "node_modules/file-type": { 187 | "version": "17.1.6", 188 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", 189 | "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", 190 | "dependencies": { 191 | "readable-web-to-node-stream": "^3.0.2", 192 | "strtok3": "^7.0.0-alpha.9", 193 | "token-types": "^5.0.0-alpha.2" 194 | }, 195 | "engines": { 196 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 197 | }, 198 | "funding": { 199 | "url": "https://github.com/sindresorhus/file-type?sponsor=1" 200 | } 201 | }, 202 | "node_modules/follow-redirects": { 203 | "version": "1.15.1", 204 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", 205 | "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", 206 | "funding": [ 207 | { 208 | "type": "individual", 209 | "url": "https://github.com/sponsors/RubenVerborgh" 210 | } 211 | ], 212 | "engines": { 213 | "node": ">=4.0" 214 | }, 215 | "peerDependenciesMeta": { 216 | "debug": { 217 | "optional": true 218 | } 219 | } 220 | }, 221 | "node_modules/form-data": { 222 | "version": "4.0.0", 223 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 224 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 225 | "dependencies": { 226 | "asynckit": "^0.4.0", 227 | "combined-stream": "^1.0.8", 228 | "mime-types": "^2.1.12" 229 | }, 230 | "engines": { 231 | "node": ">= 6" 232 | } 233 | }, 234 | "node_modules/ieee754": { 235 | "version": "1.2.1", 236 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 237 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 238 | "funding": [ 239 | { 240 | "type": "github", 241 | "url": "https://github.com/sponsors/feross" 242 | }, 243 | { 244 | "type": "patreon", 245 | "url": "https://www.patreon.com/feross" 246 | }, 247 | { 248 | "type": "consulting", 249 | "url": "https://feross.org/support" 250 | } 251 | ] 252 | }, 253 | "node_modules/inherits": { 254 | "version": "2.0.4", 255 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 256 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 257 | }, 258 | "node_modules/lodash.snakecase": { 259 | "version": "4.1.1", 260 | "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", 261 | "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" 262 | }, 263 | "node_modules/lodash.uniqwith": { 264 | "version": "4.5.0", 265 | "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", 266 | "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==" 267 | }, 268 | "node_modules/lowdb": { 269 | "version": "3.0.0", 270 | "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-3.0.0.tgz", 271 | "integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==", 272 | "dependencies": { 273 | "steno": "^2.1.0" 274 | }, 275 | "engines": { 276 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 277 | }, 278 | "funding": { 279 | "url": "https://github.com/sponsors/typicode" 280 | } 281 | }, 282 | "node_modules/mime-db": { 283 | "version": "1.50.0", 284 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", 285 | "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", 286 | "engines": { 287 | "node": ">= 0.6" 288 | } 289 | }, 290 | "node_modules/mime-types": { 291 | "version": "2.1.33", 292 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", 293 | "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", 294 | "dependencies": { 295 | "mime-db": "1.50.0" 296 | }, 297 | "engines": { 298 | "node": ">= 0.6" 299 | } 300 | }, 301 | "node_modules/peek-readable": { 302 | "version": "5.0.0", 303 | "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", 304 | "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", 305 | "engines": { 306 | "node": ">=14.16" 307 | }, 308 | "funding": { 309 | "type": "github", 310 | "url": "https://github.com/sponsors/Borewit" 311 | } 312 | }, 313 | "node_modules/readable-stream": { 314 | "version": "3.6.0", 315 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 316 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 317 | "dependencies": { 318 | "inherits": "^2.0.3", 319 | "string_decoder": "^1.1.1", 320 | "util-deprecate": "^1.0.1" 321 | }, 322 | "engines": { 323 | "node": ">= 6" 324 | } 325 | }, 326 | "node_modules/readable-web-to-node-stream": { 327 | "version": "3.0.2", 328 | "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", 329 | "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", 330 | "dependencies": { 331 | "readable-stream": "^3.6.0" 332 | }, 333 | "engines": { 334 | "node": ">=8" 335 | }, 336 | "funding": { 337 | "type": "github", 338 | "url": "https://github.com/sponsors/Borewit" 339 | } 340 | }, 341 | "node_modules/safe-buffer": { 342 | "version": "5.2.1", 343 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 344 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 345 | "funding": [ 346 | { 347 | "type": "github", 348 | "url": "https://github.com/sponsors/feross" 349 | }, 350 | { 351 | "type": "patreon", 352 | "url": "https://www.patreon.com/feross" 353 | }, 354 | { 355 | "type": "consulting", 356 | "url": "https://feross.org/support" 357 | } 358 | ] 359 | }, 360 | "node_modules/steno": { 361 | "version": "2.1.0", 362 | "resolved": "https://registry.npmjs.org/steno/-/steno-2.1.0.tgz", 363 | "integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==", 364 | "engines": { 365 | "node": "^14.13.1 || >=16.0.0" 366 | }, 367 | "funding": { 368 | "url": "https://github.com/sponsors/typicode" 369 | } 370 | }, 371 | "node_modules/streamsearch": { 372 | "version": "1.1.0", 373 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 374 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 375 | "engines": { 376 | "node": ">=10.0.0" 377 | } 378 | }, 379 | "node_modules/string_decoder": { 380 | "version": "1.3.0", 381 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 382 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 383 | "dependencies": { 384 | "safe-buffer": "~5.2.0" 385 | } 386 | }, 387 | "node_modules/strtok3": { 388 | "version": "7.0.0", 389 | "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", 390 | "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", 391 | "dependencies": { 392 | "@tokenizer/token": "^0.3.0", 393 | "peek-readable": "^5.0.0" 394 | }, 395 | "engines": { 396 | "node": ">=14.16" 397 | }, 398 | "funding": { 399 | "type": "github", 400 | "url": "https://github.com/sponsors/Borewit" 401 | } 402 | }, 403 | "node_modules/token-types": { 404 | "version": "5.0.0-alpha.2", 405 | "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.0-alpha.2.tgz", 406 | "integrity": "sha512-EsG9UxAW4M6VATrEEjhPFTKEUi1OiJqTUMIZOGBN49fGxYjZB36k0p7to3HZSmWRoHm1QfZgrg3e02fpqAt5fQ==", 407 | "dependencies": { 408 | "@tokenizer/token": "^0.3.0", 409 | "ieee754": "^1.2.1" 410 | }, 411 | "engines": { 412 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 413 | }, 414 | "funding": { 415 | "type": "github", 416 | "url": "https://github.com/sponsors/Borewit" 417 | } 418 | }, 419 | "node_modules/ts-mixer": { 420 | "version": "6.0.1", 421 | "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.1.tgz", 422 | "integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==" 423 | }, 424 | "node_modules/tslib": { 425 | "version": "2.4.0", 426 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 427 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 428 | }, 429 | "node_modules/undici": { 430 | "version": "5.15.0", 431 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.15.0.tgz", 432 | "integrity": "sha512-wCAZJDyjw9Myv+Ay62LAoB+hZLPW9SmKbQkbHIhMw/acKSlpn7WohdMUc/Vd4j1iSMBO0hWwU8mjB7a5p5bl8g==", 433 | "dependencies": { 434 | "busboy": "^1.6.0" 435 | }, 436 | "engines": { 437 | "node": ">=12.18" 438 | } 439 | }, 440 | "node_modules/util-deprecate": { 441 | "version": "1.0.2", 442 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 443 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 444 | }, 445 | "node_modules/ws": { 446 | "version": "8.8.1", 447 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", 448 | "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", 449 | "engines": { 450 | "node": ">=10.0.0" 451 | }, 452 | "peerDependencies": { 453 | "bufferutil": "^4.0.1", 454 | "utf-8-validate": "^5.0.2" 455 | }, 456 | "peerDependenciesMeta": { 457 | "bufferutil": { 458 | "optional": true 459 | }, 460 | "utf-8-validate": { 461 | "optional": true 462 | } 463 | } 464 | } 465 | }, 466 | "dependencies": { 467 | "@discordjs/builders": { 468 | "version": "1.0.0", 469 | "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.0.0.tgz", 470 | "integrity": "sha512-8y91ZfpOHubiGJu5tVyGI9tQCEyHZDTeqUWVcJd0dq7B96xIf84S0L4fwmD1k9zTe1eqEFSk0gc7BpY+FKn7Ww==", 471 | "requires": { 472 | "@sapphire/shapeshift": "^3.5.1", 473 | "discord-api-types": "^0.36.2", 474 | "fast-deep-equal": "^3.1.3", 475 | "ts-mixer": "^6.0.1", 476 | "tslib": "^2.4.0" 477 | } 478 | }, 479 | "@discordjs/collection": { 480 | "version": "1.0.0", 481 | "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.0.0.tgz", 482 | "integrity": "sha512-nAxDQYE5dNAzEGQ7HU20sujDsG5vLowUKCEqZkKUIlrXERZFTt/60zKUj/g4+AVCGeq+pXC5hivMaNtiC+PY5Q==" 483 | }, 484 | "@discordjs/rest": { 485 | "version": "1.0.0", 486 | "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-1.0.0.tgz", 487 | "integrity": "sha512-uDAvnE0P2a8axMdD4C51EGjvCRQ2HZk2Yxf6vHWZgIqG87D8DGKMPwmquIxrrB07MjV+rwci2ObU+mGhGP+bJg==", 488 | "requires": { 489 | "@discordjs/collection": "^1.0.0", 490 | "@sapphire/async-queue": "^1.3.2", 491 | "@sapphire/snowflake": "^3.2.2", 492 | "discord-api-types": "^0.36.2", 493 | "file-type": "^17.1.2", 494 | "tslib": "^2.4.0", 495 | "undici": "^5.7.0" 496 | } 497 | }, 498 | "@sapphire/async-queue": { 499 | "version": "1.3.2", 500 | "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.2.tgz", 501 | "integrity": "sha512-rUpMLATsoAMnlN3gecAcr9Ecnw1vG7zi5Xr+IX22YzRzi1k9PF9vKzoT8RuEJbiIszjcimu3rveqUnvwDopz8g==" 502 | }, 503 | "@sapphire/shapeshift": { 504 | "version": "3.5.1", 505 | "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.5.1.tgz", 506 | "integrity": "sha512-7JFsW5IglyOIUQI1eE0g6h06D/Far6HqpcowRScgCiLSqTf3hhkPWCWotVTtVycnDCMYIwPeaw6IEPBomKC8pA==", 507 | "requires": { 508 | "fast-deep-equal": "^3.1.3", 509 | "lodash.uniqwith": "^4.5.0" 510 | } 511 | }, 512 | "@sapphire/snowflake": { 513 | "version": "3.2.2", 514 | "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.2.tgz", 515 | "integrity": "sha512-ula2O0kpSZtX9rKXNeQMrHwNd7E4jPDJYUXmEGTFdMRfyfMw+FPyh04oKMjAiDuOi64bYgVkOV3MjK+loImFhQ==" 516 | }, 517 | "@tokenizer/token": { 518 | "version": "0.3.0", 519 | "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", 520 | "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" 521 | }, 522 | "@types/node": { 523 | "version": "18.0.6", 524 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.6.tgz", 525 | "integrity": "sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw==" 526 | }, 527 | "@types/ws": { 528 | "version": "8.5.3", 529 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", 530 | "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", 531 | "requires": { 532 | "@types/node": "*" 533 | } 534 | }, 535 | "asynckit": { 536 | "version": "0.4.0", 537 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 538 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 539 | }, 540 | "axios": { 541 | "version": "0.27.2", 542 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", 543 | "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", 544 | "requires": { 545 | "follow-redirects": "^1.14.9", 546 | "form-data": "^4.0.0" 547 | } 548 | }, 549 | "busboy": { 550 | "version": "1.6.0", 551 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 552 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 553 | "requires": { 554 | "streamsearch": "^1.1.0" 555 | } 556 | }, 557 | "combined-stream": { 558 | "version": "1.0.8", 559 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 560 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 561 | "requires": { 562 | "delayed-stream": "~1.0.0" 563 | } 564 | }, 565 | "delayed-stream": { 566 | "version": "1.0.0", 567 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 568 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 569 | }, 570 | "discord-api-types": { 571 | "version": "0.36.2", 572 | "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.2.tgz", 573 | "integrity": "sha512-TunPAvzwneK/m5fr4hxH3bMsrtI22nr9yjfHyo5NBGMjpsAauGNiGCmwoFf0oO3jSd2mZiKUvZwCKDaB166u2Q==" 574 | }, 575 | "discord.js": { 576 | "version": "14.0.3", 577 | "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.0.3.tgz", 578 | "integrity": "sha512-wH/VQl4CqN8/+dcXEtYis1iurqxGlDpEe0O4CqH5FGqZGIjVpTdtK0STXXx7bVNX8MT/0GvLZLkmO/5gLDWZVg==", 579 | "requires": { 580 | "@discordjs/builders": "^1.0.0", 581 | "@discordjs/collection": "^1.0.0", 582 | "@discordjs/rest": "^1.0.0", 583 | "@sapphire/snowflake": "^3.2.2", 584 | "@types/ws": "^8.5.3", 585 | "discord-api-types": "^0.36.2", 586 | "fast-deep-equal": "^3.1.3", 587 | "lodash.snakecase": "^4.1.1", 588 | "tslib": "^2.4.0", 589 | "undici": "^5.8.0", 590 | "ws": "^8.8.1" 591 | } 592 | }, 593 | "fast-deep-equal": { 594 | "version": "3.1.3", 595 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 596 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 597 | }, 598 | "file-type": { 599 | "version": "17.1.6", 600 | "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", 601 | "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", 602 | "requires": { 603 | "readable-web-to-node-stream": "^3.0.2", 604 | "strtok3": "^7.0.0-alpha.9", 605 | "token-types": "^5.0.0-alpha.2" 606 | } 607 | }, 608 | "follow-redirects": { 609 | "version": "1.15.1", 610 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", 611 | "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" 612 | }, 613 | "form-data": { 614 | "version": "4.0.0", 615 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 616 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 617 | "requires": { 618 | "asynckit": "^0.4.0", 619 | "combined-stream": "^1.0.8", 620 | "mime-types": "^2.1.12" 621 | } 622 | }, 623 | "ieee754": { 624 | "version": "1.2.1", 625 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 626 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 627 | }, 628 | "inherits": { 629 | "version": "2.0.4", 630 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 631 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 632 | }, 633 | "lodash.snakecase": { 634 | "version": "4.1.1", 635 | "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", 636 | "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" 637 | }, 638 | "lodash.uniqwith": { 639 | "version": "4.5.0", 640 | "resolved": "https://registry.npmjs.org/lodash.uniqwith/-/lodash.uniqwith-4.5.0.tgz", 641 | "integrity": "sha512-7lYL8bLopMoy4CTICbxygAUq6CdRJ36vFc80DucPueUee+d5NBRxz3FdT9Pes/HEx5mPoT9jwnsEJWz1N7uq7Q==" 642 | }, 643 | "lowdb": { 644 | "version": "3.0.0", 645 | "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-3.0.0.tgz", 646 | "integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==", 647 | "requires": { 648 | "steno": "^2.1.0" 649 | } 650 | }, 651 | "mime-db": { 652 | "version": "1.50.0", 653 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", 654 | "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" 655 | }, 656 | "mime-types": { 657 | "version": "2.1.33", 658 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", 659 | "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", 660 | "requires": { 661 | "mime-db": "1.50.0" 662 | } 663 | }, 664 | "peek-readable": { 665 | "version": "5.0.0", 666 | "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", 667 | "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==" 668 | }, 669 | "readable-stream": { 670 | "version": "3.6.0", 671 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 672 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 673 | "requires": { 674 | "inherits": "^2.0.3", 675 | "string_decoder": "^1.1.1", 676 | "util-deprecate": "^1.0.1" 677 | } 678 | }, 679 | "readable-web-to-node-stream": { 680 | "version": "3.0.2", 681 | "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", 682 | "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", 683 | "requires": { 684 | "readable-stream": "^3.6.0" 685 | } 686 | }, 687 | "safe-buffer": { 688 | "version": "5.2.1", 689 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 690 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 691 | }, 692 | "steno": { 693 | "version": "2.1.0", 694 | "resolved": "https://registry.npmjs.org/steno/-/steno-2.1.0.tgz", 695 | "integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==" 696 | }, 697 | "streamsearch": { 698 | "version": "1.1.0", 699 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 700 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" 701 | }, 702 | "string_decoder": { 703 | "version": "1.3.0", 704 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 705 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 706 | "requires": { 707 | "safe-buffer": "~5.2.0" 708 | } 709 | }, 710 | "strtok3": { 711 | "version": "7.0.0", 712 | "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", 713 | "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", 714 | "requires": { 715 | "@tokenizer/token": "^0.3.0", 716 | "peek-readable": "^5.0.0" 717 | } 718 | }, 719 | "token-types": { 720 | "version": "5.0.0-alpha.2", 721 | "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.0-alpha.2.tgz", 722 | "integrity": "sha512-EsG9UxAW4M6VATrEEjhPFTKEUi1OiJqTUMIZOGBN49fGxYjZB36k0p7to3HZSmWRoHm1QfZgrg3e02fpqAt5fQ==", 723 | "requires": { 724 | "@tokenizer/token": "^0.3.0", 725 | "ieee754": "^1.2.1" 726 | } 727 | }, 728 | "ts-mixer": { 729 | "version": "6.0.1", 730 | "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.1.tgz", 731 | "integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==" 732 | }, 733 | "tslib": { 734 | "version": "2.4.0", 735 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", 736 | "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" 737 | }, 738 | "undici": { 739 | "version": "5.15.0", 740 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.15.0.tgz", 741 | "integrity": "sha512-wCAZJDyjw9Myv+Ay62LAoB+hZLPW9SmKbQkbHIhMw/acKSlpn7WohdMUc/Vd4j1iSMBO0hWwU8mjB7a5p5bl8g==", 742 | "requires": { 743 | "busboy": "^1.6.0" 744 | } 745 | }, 746 | "util-deprecate": { 747 | "version": "1.0.2", 748 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 749 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 750 | }, 751 | "ws": { 752 | "version": "8.8.1", 753 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", 754 | "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", 755 | "requires": {} 756 | } 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syncbot", 3 | "version": "1.0.0", 4 | "description": "A bot that syncs roles between one main server, and multiple other discord servers.", 5 | "main": "run.js", 6 | "scripts": { 7 | "test": "node run.js", 8 | "runReverse": "node runReverse.js", 9 | "start": "node run.js" 10 | }, 11 | "engines": { 12 | "node": "16.x" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/jonathonor/syncbot.git" 17 | }, 18 | "keywords": [ 19 | "discord", 20 | "sync", 21 | "bot", 22 | "server", 23 | "week" 24 | ], 25 | "author": "jonathonor", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/jonathonor/syncbot/issues" 29 | }, 30 | "type": "module", 31 | "homepage": "https://github.com/jonathonor/syncbot#readme", 32 | "dependencies": { 33 | "@discordjs/rest": "^1.0.0", 34 | "axios": "^0.27.2", 35 | "discord-api-types": "^0.36.2", 36 | "discord.js": "^14.0.3", 37 | "lowdb": "^3.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /register.js: -------------------------------------------------------------------------------- 1 | import { REST } from "@discordjs/rest"; 2 | import { Routes } from "discord-api-types/v9"; 3 | import { createRequire } from "module"; 4 | const require = createRequire(import.meta.url); 5 | var config = require('./config.json') 6 | 7 | const commands = [{ 8 | name: 'add', 9 | description: 'Will add the role to the user in both servers.', 10 | options: [{ 11 | "name": "role", 12 | "description": "The role to add in both servers", 13 | "type": 8, 14 | "required": true 15 | }, 16 | { 17 | "name": "user", 18 | "description": "The user to add the role to in both servers", 19 | "type": 6, 20 | "required": true 21 | }] 22 | }, { 23 | name: 'remove', 24 | description: 'Will remove the role from the user in both servers.', 25 | options: [{ 26 | "name": "role", 27 | "description": "The role to remove in both servers", 28 | "type": 8, 29 | "required": true 30 | }, 31 | { 32 | "name": "user", 33 | "description": "The user to remove the role to in both servers", 34 | "type": 6, 35 | "required": true 36 | }] 37 | }, { 38 | "name": "role-checker", 39 | "type": 1, 40 | "description": "A role checker to compare roles between the main server and synced servers.", 41 | "options": [ 42 | { 43 | "name": "option", 44 | "description": "Analyze sends a DM with the differences, force-sync will apply the changes shown in the analysis", 45 | "type": 3, 46 | "required": true, 47 | "choices": [ 48 | { 49 | "name": "analyze", 50 | "value": "analyze" 51 | }, 52 | { 53 | "name": "force-sync", 54 | "value": "force" 55 | } 56 | ] 57 | } 58 | ] 59 | }]; 60 | 61 | const rest = new REST({ version: '9' }).setToken(config.token); 62 | 63 | (async () => { 64 | try { 65 | console.log('Started refreshing application (/) commands.'); 66 | 67 | await rest.put( 68 | Routes.applicationGuildCommands(config.applicationId, config.mainServer), 69 | { body: commands }, 70 | ); 71 | 72 | console.log('Successfully reloaded application (/) commands.'); 73 | } catch (error) { 74 | console.error(error); 75 | } 76 | })(); -------------------------------------------------------------------------------- /registerGlobal.js: -------------------------------------------------------------------------------- 1 | import { REST } from "@discordjs/rest"; 2 | import { Routes } from "discord-api-types/v9"; 3 | import { createRequire } from "module"; 4 | const require = createRequire(import.meta.url); 5 | var config = require('./config.json') 6 | 7 | const commands = [{ 8 | name: 'add', 9 | description: 'Will add the role to the user in both servers.', 10 | options: [{ 11 | "name": "role", 12 | "description": "The role to add in both servers", 13 | "type": 8, 14 | "required": true 15 | }, 16 | { 17 | "name": "user", 18 | "description": "The user to add the role to in both servers", 19 | "type": 6, 20 | "required": true 21 | }] 22 | }, { 23 | name: 'remove', 24 | description: 'Will remove the role from the user in both servers.', 25 | options: [{ 26 | "name": "role", 27 | "description": "The role to remove in both servers", 28 | "type": 8, 29 | "required": true 30 | }, 31 | { 32 | "name": "user", 33 | "description": "The user to remove the role to in both servers", 34 | "type": 6, 35 | "required": true 36 | }] 37 | }, { 38 | "name": "role-checker", 39 | "type": 1, 40 | "description": "A role checker to compare roles between the main server and synced servers.", 41 | "options": [ 42 | { 43 | "name": "option", 44 | "description": "Analyze sends a DM with the differences, force-sync will apply the changes shown in the analysis", 45 | "type": 3, 46 | "required": true, 47 | "choices": [ 48 | { 49 | "name": "analyze", 50 | "value": "analyze" 51 | }, 52 | { 53 | "name": "force-sync", 54 | "value": "force" 55 | } 56 | ] 57 | } 58 | ] 59 | }]; 60 | 61 | const rest = new REST({ version: '9' }).setToken(config.token); 62 | 63 | (async () => { 64 | try { 65 | console.log('Started refreshing application (/) commands.'); 66 | 67 | // TODO: do we want global, or for each of the synced servers if logic reversed 68 | await rest.put( 69 | Routes.applicationCommands(config.applicationId), 70 | { body: commands }, 71 | ); 72 | 73 | console.log('Successfully reloaded application (/) commands.'); 74 | } catch (error) { 75 | console.error(error); 76 | } 77 | })(); -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | /* 2 | syncBot, a super simple bot that gives you the ability to add/remove a role of a 3 | member in two servers at the same time. 4 | */ 5 | 6 | // Use cases: 7 | // 1. User role is added in the main server manually, if the user exists in any synced servers, any role with the same name will be applied in the synced servers 8 | // 2. User role is added via slash command in the main server, if the user exists in any synced servers, the bot will apply the role with the same name in each synced server 9 | // 3. User role is removed in the main server manually, any role with the same name is removed from the user in all synced servers 10 | // 4. User role is removed via slash command in the main server, any role with the same name is removed from the user in each synced server 11 | // 5. User is added to a synced server, roles that match names of roles the user has in the main server are applied to the user in the synced servers 12 | // 6. User is removed from the main server, role names that the user has in the main server are removed in synced servers 13 | import { createRequire } from "module"; 14 | const require = createRequire(import.meta.url); 15 | var config = require('./config.json') 16 | import { 17 | Client, GatewayIntentBits 18 | } from "discord.js"; 19 | import { colorLog } from "./helpers.js"; 20 | const axios = require('axios') 21 | const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); 22 | // This is to keep the action from firing twice when using the (/) command, since the guildMemberUpdate will see the role update and fire the add/remove again. 23 | let triggeredByIntention = false; 24 | const isDebug = process.argv[2] === 'debug'; 25 | 26 | let verifyConfig = () => { 27 | console.log("\x1b[91mError \x1b[93mWarning \x1b[94mInfo\x1b[0m") 28 | 29 | let hasError = false; 30 | colorLog('info', 'VERIFYING CONFIG FILE'); 31 | 32 | //errors 33 | if (!config.applicationId) { 34 | hasError = true; 35 | colorLog('error', 'Config applicationId missing, please check.'); 36 | } 37 | 38 | if (!config.token) { 39 | hasError = true; 40 | colorLog('error', 'Config token missing, please add it.'); 41 | } 42 | 43 | if (!config.mainServer) { 44 | hasError = true; 45 | colorLog('error', 'Config mainserver missing, please check.'); 46 | } 47 | 48 | if (!config.syncedServers || (config.syncedServers && !Array.isArray(config.syncedServers))) { 49 | hasError = true; 50 | colorLog('error', 'Config syncedServers missing or not Array, please verify it exists and matches structure "syncedServers": ["123456789123456789"], '); 51 | } 52 | 53 | // warnings 54 | if (!config.logChannelId) { 55 | colorLog('warning', 'logChannelId not found in config file, logs will not be sent.'); 56 | } 57 | 58 | if (!config.allowedRoleName) { 59 | if (!config.allowedRoleId) { 60 | colorLog('warning', 'allowedRoleName and allowedRoleId not found in config file, only server owner can use commands.'); 61 | } 62 | colorLog('warning', 'allowedRoleName not found in config file, only server owner can use commands or user with allowedRoleId.'); 63 | } 64 | 65 | if (!config.allowedRoleId) { 66 | if (!config.allowedRoleName) { 67 | colorLog('warning', 'allowedRoleId and allowedRoleName not found in config file, only server owner can use commands.'); 68 | } 69 | colorLog('warning', 'allowedRoleId not found in config file, only server owner can use commands or user with allowedRoleName.'); 70 | } 71 | 72 | if (hasError) { 73 | colorLog('error', 'CONFIG FILE HAD ERROR, EXITING!'); 74 | process.exit(1); 75 | } else { 76 | colorLog('info', 'FINISHED VERIFYING CONFIG FILE'); 77 | } 78 | } 79 | 80 | verifyConfig(); 81 | 82 | client.on('ready', async () => { 83 | let hasError = false; 84 | console.log(`syncbot ready!`); 85 | console.log(`debug mode set to ${isDebug}`); 86 | 87 | colorLog('info', `VERIFYING BOT IS IN ALL SERVER ID's IN CONFIG FILE`); 88 | const guildsBotIsIn = await client.guilds.fetch(); 89 | if (!guildsBotIsIn.findKey(guild => guild.id === config.mainServer)) { 90 | hasError = true; 91 | colorLog('error', `Bot is not in main server with id: ${config.mainServer} Please invite bot to server and restart bot.`); 92 | } 93 | 94 | for (const serverId of config.syncedServers) { 95 | if (!guildsBotIsIn.findKey(guild => guild.id === serverId)) { 96 | hasError = true; 97 | colorLog('error', `Bot is not in synced server ${serverId}: Please invite bot to server and restart bot.`); 98 | } 99 | } 100 | 101 | if (hasError) { 102 | colorLog('error', 'BOT NOT IN A SERVER, EXITING!'); 103 | process.exit(1); 104 | } else { 105 | colorLog('info', 'FINISHED VERIFYING BOT IS IN ALL SERVERS FROM CONFIG FILE'); 106 | } 107 | }); 108 | 109 | client.on('interactionCreate', async interaction => { 110 | // 2 === APPLICATION_COMMAND 111 | if (interaction.type !== 2) return; 112 | if (!interaction.guildId) { 113 | respondToInteraction(interaction, 'This command must be sent from a guild/server.'); 114 | } 115 | 116 | if (interaction.commandName === 'add') { 117 | verifyUser(interaction.member.id).then(async verified => { 118 | if (verified) { 119 | let member = interaction.options.data.find(obj => obj.name === 'user').member; 120 | let role = interaction.options.data.find(obj => obj.name === 'role').value; 121 | triggeredByIntention = true; 122 | addRole(member, role, interaction); 123 | } else { 124 | respondToInteraction(interaction, `You dont have the necessary role to send that command ${interaction.user.username}`); 125 | } 126 | }); 127 | } 128 | 129 | if (interaction.commandName === 'remove') { 130 | verifyUser(interaction.member.id).then(async verified => { 131 | if (verified) { 132 | let member = interaction.options.data.find(obj => obj.name === 'user').member; 133 | let role = interaction.options.data.find(obj => obj.name === 'role').value; 134 | triggeredByIntention = true; 135 | removeRole(member, role, interaction); 136 | } else { 137 | respondToInteraction(interaction, `You dont have the necessary role to send that command ${interaction.user.username}`); 138 | } 139 | }); 140 | } 141 | 142 | if (interaction.commandName === 'role-checker') { 143 | verifyUser(interaction.member.id).then(async verified => { 144 | 145 | if (verified && (interaction.guildId === config.mainServer)) { 146 | let option = interaction.options.data.find(obj => obj.name === 'option').value; 147 | triggeredByIntention = true; 148 | debugLog(`${interaction.member.displayName} is verified, running role checker with ${option}`); 149 | 150 | if (interaction.guild.memberCount > 100) { 151 | await interaction.reply({content: `This may take a while since you have ${interaction.guild.memberCount}, I'll send you a DM when I'm done.`, ephemeral: true}); 152 | } else { 153 | await interaction.deferReply({ ephemeral: true }); 154 | } 155 | 156 | if (option === 'analyze') 157 | { 158 | await newAnalyze(interaction, false); 159 | } else if (option === 'force') { 160 | await newAnalyze(interaction, true); 161 | } 162 | } else { 163 | if (!verified) { 164 | respondToInteraction(interaction, `You dont have the necessary role to send that command ${interaction.user.username}`); 165 | } else { 166 | respondToInteraction(interaction, `You need to run this command in your main server.`); 167 | } 168 | } 169 | }); 170 | } 171 | }); 172 | 173 | let newAnalyze = async (interaction, forceSync) => { 174 | let data = { membersAnalyzed: 0, membersWithDifferences: [], errors: [] }; 175 | let startTime = new Date(); 176 | 177 | let mainServerMembers = await interaction.guild.members.fetch(); 178 | let mainServerRoles = await interaction.guild.roles.fetch(); 179 | let mainServerRoleNames = mainServerRoles.map(r => r.name); 180 | let mainServerPremiumRole = interaction.guild.roles.premiumSubscriberRole; // for testing nitro{ id: 'mainPremium', name: 'serverBooster'} 181 | 182 | const mainServerMe = await interaction.guild.members.fetchMe(); 183 | const mainServerMeRole = mainServerMe.roles.botRole; 184 | const mainServerRolesHigherThanBot = mainServerRoles 185 | .filter(r => r.comparePositionTo(mainServerMeRole) > 0) 186 | .map(r => r.name); 187 | 188 | let hasDifferingRoles = false; 189 | for (const server of config.syncedServers) { 190 | let syncedServer = await client.guilds.fetch(server); 191 | let syncedServerMembers = await syncedServer.members.fetch(); 192 | let syncedServerRoles = await syncedServer.roles.fetch(); 193 | let syncedServerRoleNames = syncedServerRoles.map(r => r.name); 194 | let syncedServerPremiumRole = syncedServer.roles.premiumSubscriberRole; // for testing nitro{ id: 'syncedPremium', name: 'serverBooster'} 195 | 196 | const syncedMe = await syncedServer.members.fetchMe(); 197 | const syncedMeRole = syncedMe.roles.botRole; 198 | const syncedServerRolesHigherThanBot = syncedServerRoles 199 | .filter(r => r.comparePositionTo(syncedMeRole) > 0) 200 | .map(r => r.name); 201 | 202 | for (const syncedMember of syncedServerMembers.values()) { 203 | if (syncedMember.manageable && !syncedMember.user.bot) { 204 | let memberObj = {username: interaction.member.displayName, serversWithDifferingRoles: []}; 205 | 206 | let syncedMemberRoles = syncedMember.roles.cache; 207 | let syncedMemberRoleNames = syncedMemberRoles.map(r => r.name); 208 | let mainServerMember = mainServerMembers.get(syncedMember.id); 209 | 210 | if (mainServerMember) { 211 | let mainServerMemberRoles = mainServerMember.roles.cache; 212 | let mainServerMemberRoleNames = mainServerMemberRoles.map(r => r.name); 213 | 214 | let roleCollectionToRemove = syncedMemberRoles 215 | .filter(r => mainServerRoleNames.includes(r.name) && !mainServerMemberRoleNames.includes(r.name)) 216 | .filter(r => !mainServerRolesHigherThanBot.includes(r.name)) 217 | .filter(r => syncedServerPremiumRole && (r.name !== syncedServerPremiumRole.name)); 218 | 219 | let roleCollectionToAdd = mainServerMemberRoles 220 | .filter(r => syncedServerRoleNames.includes(r.name) && !syncedMemberRoleNames.includes(r.name)) 221 | .filter(r => !syncedServerRolesHigherThanBot.includes(r.name)) 222 | .filter(r => mainServerPremiumRole && (r.name !== mainServerPremiumRole.name)) 223 | .map(role => syncedServerRoles.find(r => r.name === role.name)); 224 | 225 | let rolesToRemoveInThisServer = [...roleCollectionToRemove.values()]; 226 | let roleNamesToRemoveInThisServer = rolesToRemoveInThisServer.map(r => r.name); 227 | debugLog(`Roles ${syncedMember.displayName} has in ${syncedServer.name} but not in mainserver: ${roleNamesToRemoveInThisServer}`); 228 | 229 | let rolesToAddInThisServer = [...roleCollectionToAdd.values()]; 230 | let roleNamesToAddInThisServer = rolesToAddInThisServer.map(r => r.name); 231 | debugLog(`Roles ${syncedMember.displayName} has in mainserver but not in ${syncedServer.name}: ${roleNamesToAddInThisServer}`); 232 | 233 | 234 | if (rolesToRemoveInThisServer.length > 0 || rolesToAddInThisServer.length > 0) { 235 | hasDifferingRoles = true; 236 | let remove = forceSync ? 'rolesRemovedToMatchMainserver' : 'rolesToRemoveToMatchMainserver'; 237 | let add = forceSync ? 'rolesAddedToMatchMainserver' : 'rolesToAddToMatchMainServer'; 238 | if (rolesToRemoveInThisServer.length > 0 && rolesToAddInThisServer.length === 0) { 239 | debugLog(`Roles to be removed: ${roleNamesToRemoveInThisServer} from ${syncedMember.displayName} in ${syncedServer.name}`) 240 | if (forceSync) { 241 | await syncedMember.roles.remove(rolesToRemoveInThisServer); 242 | } 243 | 244 | memberObj.serversWithDifferingRoles 245 | .push({ serverName: syncedServer.name, 246 | [`${remove}`]: roleNamesToRemoveInThisServer, 247 | }); 248 | } 249 | if (rolesToAddInThisServer.length > 0 && rolesToRemoveInThisServer.length === 0) { 250 | debugLog(`Roles to be added: ${roleNamesToAddInThisServer} to ${syncedMember.displayName} in ${syncedServer.name}`) 251 | if (forceSync) { 252 | await syncedMember.roles.add(rolesToAddInThisServer); 253 | } 254 | 255 | memberObj.serversWithDifferingRoles 256 | .push({ serverName: syncedServer.name, 257 | [`${add}`]: roleNamesToAddInThisServer, 258 | }); 259 | } 260 | if (rolesToAddInThisServer.length > 0 && rolesToRemoveInThisServer.length > 0) { 261 | debugLog(`Roles to be added and removed for ${syncedMember.displayName} in ${syncedServer.name}`) 262 | debugLog(`Add: ${roleNamesToAddInThisServer}`) 263 | debugLog(`Remove: ${roleNamesToRemoveInThisServer}`) 264 | if (forceSync) { 265 | debugLog(`Force syncing combination for: ${syncedMember.displayName}`) 266 | await syncedMember.roles.remove(rolesToRemoveInThisServer); 267 | await syncedMember.roles.add(rolesToAddInThisServer); 268 | } 269 | 270 | memberObj.serversWithDifferingRoles 271 | .push({ serverName: syncedServer.name, 272 | [`${remove}`]: roleNamesToRemoveInThisServer, 273 | [`${add}`]: roleNamesToAddInThisServer 274 | }); 275 | } 276 | } 277 | } else { 278 | // await member.roles.set([]); 279 | debugLog(`${syncedMember.displayName} does not exist in main server`); 280 | } 281 | if (hasDifferingRoles) { 282 | debugLog(`${syncedMember.displayName} in ${syncedServer.name} has differing roles from mainserver`) 283 | data.membersWithDifferences.push(memberObj); 284 | } 285 | } else { 286 | data.errors.push(`${syncedMember.displayName} is not manageable or is a bot.`); 287 | debugLog(`${syncedMember.displayName} is not manageable or is a bot.`) 288 | } 289 | data.membersAnalyzed++; 290 | } 291 | } 292 | var delta = Math.abs(new Date().getTime() - startTime.getTime()) / 1000; 293 | var minutes = Math.floor(delta / 60) % 60; 294 | delta -= minutes * 60; 295 | var seconds = delta % 60; 296 | 297 | if (interaction.isRepliable()) { 298 | await interaction.editReply(`I finished processing ${data.membersAnalyzed} members in ${minutes} minutes and ${seconds} seconds. There were ${data.errors.length} errors.`); 299 | } 300 | triggeredByIntention = false; 301 | return interaction.user 302 | .createDM() 303 | .then((dmChannel) => { 304 | var buf = Buffer.from(JSON.stringify(data, null, 4)); 305 | dmChannel.send({ 306 | files: [ 307 | { 308 | attachment: buf, 309 | name: `${interaction.guild.name}.json`, 310 | }, 311 | ], 312 | }); 313 | }); 314 | }; 315 | 316 | // Manual function registered to (/) slash command to add a role from a user across all synced servers 317 | let addRole = async (member, roleId, interaction = null) => { 318 | const mainServer = await client.guilds.fetch(config.mainServer); 319 | const mainServerRoleToAdd = await mainServer.roles.fetch(roleId); 320 | 321 | if (!!interaction) { 322 | member.roles.add(mainServerRoleToAdd).catch(err => respondToInteraction(interaction, 'There was an error adding the role in the main server, see console for error', err)); 323 | } 324 | 325 | for (const server of config.syncedServers) { 326 | // TODO: if bot is not in server, this will fail (user has syncedServer id that they didn't invite bot to) 327 | const serverToSync = await client.guilds.fetch(server); 328 | const serverToSyncRoles = await serverToSync.roles.fetch(); 329 | const syncedServerRoleToAdd = serverToSyncRoles.find(r => r.name === mainServerRoleToAdd.name); 330 | let memberToSync = serverToSync.members.cache.find(m => m.id === member.id); 331 | if (memberToSync && syncedServerRoleToAdd) { 332 | memberToSync.roles.add(syncedServerRoleToAdd).catch(err => respondToInteraction(interaction, `There was an error adding the role in a synced server named: ${serverToSync.name}, see console for error`, err)); 333 | respondToInteraction(interaction, `Added ${mainServerRoleToAdd.name} to ${member.user.username} in ${serverToSync.name}`); 334 | } else if (!syncedServerRoleToAdd) { 335 | respondToInteraction(interaction, `Unable to add role ${mainServerRoleToAdd.name} to ${member.user.username} in ${serverToSync.name}, role does not exist.`); 336 | } else { 337 | serverToSync.members.fetch().then(updatedMembers => { 338 | let updatedMember = updatedMembers.find(m => m.id === member.id); 339 | if (updatedMember && syncedServerRoleToAdd) { 340 | updatedMember.roles.add(syncedServerRoleToAdd).catch(err => respondToInteraction(interaction, 'There was an error adding the role in the secondary server after fetching all users, see console for error', err)); 341 | respondToInteraction(interaction, `Added ${mainServerRoleToAdd.name} to ${member.user.username} in ${serverToSync.name}`); 342 | } else { 343 | respondToInteraction(interaction, `Unable to add role ${mainServerRoleToAdd.name} to ${member.user.username} in ${serverToSync.name}, member does not exist.`); 344 | } 345 | }); 346 | } 347 | } 348 | } 349 | 350 | // Manual function registered to (/) slash command to remove a role from a user across all synced servers 351 | let removeRole = async (member, roleId, interaction = null) => { 352 | const mainServer = await client.guilds.fetch(config.mainServer); 353 | const mainServerRoleToRemove = await mainServer.roles.fetch(roleId); 354 | 355 | if (!!interaction) { 356 | member.roles.remove(mainServerRoleToRemove).catch(err => respondToInteraction(interaction, 'There was an error removing the role in the main server, see console for error', err)); 357 | } 358 | 359 | for (const server of config.syncedServers) { 360 | const serverToSync = await client.guilds.fetch(server); 361 | const serverToSyncRoles = await serverToSync.roles.fetch(); 362 | const syncedServerRoleToRemove = serverToSyncRoles.find(r => r.name === mainServerRoleToRemove.name); 363 | let memberToSync = serverToSync.members.cache.find(m => m.id === member.id); 364 | 365 | if (memberToSync && syncedServerRoleToRemove) { 366 | let memberHasRole = memberToSync.roles.cache.find(a => a.name === syncedServerRoleToRemove.name); 367 | 368 | if (memberHasRole) { 369 | memberToSync.roles.remove(syncedServerRoleToRemove).then(() => { 370 | respondToInteraction(interaction, `Removed ${mainServerRoleToRemove.name} from ${member.user.username} in ${serverToSync.name}`); 371 | }).catch(err => respondToInteraction(interaction, `There was an error removing the role in a synced server named: ${serverToSync.name}, see console for error`, err)); 372 | } else { 373 | respondToInteraction(interaction, `${member.user.username} did not have role: ${mainServerRoleToRemove.name} in ${serverToSync.name} to remove.`); 374 | } 375 | 376 | } else if (!syncedServerRoleToRemove) { 377 | respondToInteraction(interaction, `Unable to remove role ${mainServerRoleToRemove.name} from ${member.user.username} in ${serverToSync.name}, role does not exist.`); 378 | } else { 379 | serverToSync.members.fetch().then(updatedMembers => { 380 | let updatedMember = updatedMembers.find(m => m.id === member.id); 381 | if (updatedMember && syncedServerRoleToRemove) { 382 | updatedMember.roles.remove(syncedServerRoleToRemove).catch(err => respondToInteraction(interaction, 'There was an error removing the role in the secondary server after fetching all users, see console for error', err)); 383 | respondToInteraction(interaction, `Removed ${mainServerRoleToRemove.name} from ${member.user.username} in ${serverToSync.name}`); 384 | } else { 385 | respondToInteraction(interaction, `Unable to remove role ${mainServerRoleToRemove.name} from ${member.user.username} in ${serverToSync.name}, member does not exist.`); 386 | } 387 | }); 388 | } 389 | } 390 | } 391 | 392 | // When a users roles are updated in the main server, update them in all synced servers. 393 | client.on('guildMemberUpdate', (oldMember, updatedMember) => { 394 | if (!triggeredByIntention && (updatedMember.guild.id === config.mainServer)) { 395 | const oldRoles = oldMember.roles.cache; 396 | const newRoles = updatedMember.roles.cache; 397 | 398 | let oldRolesIds = oldRoles.map(r => r.id); 399 | let newRolesIds = newRoles.map(r => r.id); 400 | 401 | if (oldRolesIds.length > newRolesIds.length) { 402 | let roleToRemove = oldRoles.filter(role => !newRolesIds.includes(role.id)).first(); 403 | removeRole(updatedMember, roleToRemove.id); 404 | } 405 | 406 | if (oldRolesIds.length < newRolesIds.length) { 407 | let roleToAdd = newRoles.filter(role => !oldRolesIds.includes(role.id)).first(); 408 | addRole(updatedMember, roleToAdd.id); 409 | } 410 | } 411 | }); 412 | 413 | // When a new user joins a synced server, then look for that users roles in the main server and apply them in the synced server. 414 | client.on('guildMemberAdd', async addedMember => { 415 | debugLog(`${addedMember.displayName} joined ${addedMember.guild.name}`); 416 | if (config.syncedServers.includes(addedMember.guild.id)) { 417 | debugLog(`Config lists ${addedMember.guild.name} as synced server.`); 418 | const mainServer = await client.guilds.fetch(config.mainServer); 419 | debugLog(`Fetched mainserver: ${mainServer.name}`); 420 | mainServer.members.fetch(addedMember.user.id).then(async mainServerMember => { 421 | debugLog(`Found member in mainserver: ${mainServerMember.displayName}`); 422 | let mainServerMemberRoles = mainServerMember.roles.cache; 423 | let mainServerMemberRolesFiltered = mainServerMemberRoles.filter(r => r.name !== '@everyone'); 424 | debugLog(`Found ${mainServerMemberRolesFiltered.size} member roles for ${addedMember.displayName} in mainserver: ${mainServerMemberRolesFiltered.map(r => r.name)}`); 425 | const guildToSync = addedMember.guild; 426 | let memberToSync = addedMember; 427 | 428 | if (mainServerMemberRolesFiltered.size > 0) { 429 | debugLog(`Adding roles from mainserver: ${mainServerMemberRolesFiltered.map(r => r.name)} for ${addedMember.displayName} in ${addedMember.guild.name}`); 430 | 431 | let guildToSyncRoles = await guildToSync.roles.fetch(); 432 | const logChannel = await mainServer.channels.fetch(config.logChannelId); 433 | 434 | mainServerMemberRolesFiltered.forEach(role => { 435 | let roleToAdd = guildToSyncRoles.find(r => r.name === role.name); 436 | if (roleToAdd && roleToAdd.id && roleToAdd.name) { 437 | memberToSync.roles.add(roleToAdd).catch(err => console.log(err)); 438 | } 439 | }); 440 | logChannel.send(`Syncing roles in server: ${guildToSync.name} for new member: ${memberToSync.user.username}`); 441 | } 442 | }).catch(e => { 443 | console.log(e); 444 | debugLog(`Not adding any roles for ${addedMember.displayName} because they aren't in the main server`); 445 | }); 446 | } 447 | }); 448 | 449 | // When a user leaves the main server, then remove all of matching roles from all synced servers. 450 | client.on('guildMemberRemove', async removedMember => { 451 | if (removedMember.guild.id === config.mainServer) { 452 | debugLog(`${removedMember.displayName} left mainserver: ${removedMember.guild.name}`); 453 | 454 | const mainServer = await client.guilds.fetch(config.mainServer); 455 | let mainServerMember = removedMember; 456 | let mainServerMemberRoles = mainServerMember.roles.cache; 457 | let mainServerMemberRoleIds = mainServerMemberRoles.filter(r => r.name !== '@everyone').map(r => r.id); 458 | let mainServerRoles = await mainServer.roles.fetch(); 459 | let mainServerRoleNames = mainServerMemberRoles.filter(r => r.name !== '@everyone').map(r => r.name); 460 | const logChannel = await mainServer.channels.fetch(config.logChannelId); 461 | 462 | for (const server of config.syncedServers) { 463 | const guildToSync = await client.guilds.fetch(server); 464 | debugLog(`Removing roles ${mainServerRoleNames} from ${removedMember.displayName} in: ${guildToSync.name}`); 465 | 466 | guildToSync.members.fetch(removedMember.user.id).then(async memberToSync => { 467 | debugLog(`Removing ${mainServerRoleNames} from ${removedMember.displayName} in ${guildToSync.name}`); 468 | if (mainServerMemberRoleIds.length > 0) { 469 | let syncedServerRoles = await guildToSync.roles.fetch(); 470 | mainServerMemberRoleIds.forEach(roleId => { 471 | let mainServerRole = mainServerRoles.find(r => r.id === roleId); 472 | let roleToRemove = syncedServerRoles.find(r => r.name === mainServerRole.name); 473 | if (roleToRemove) { 474 | memberToSync.roles.remove(roleToRemove).catch(err => console.log(err)); 475 | } 476 | }); 477 | logChannel.send(`Removing roles from: ${memberToSync.user.username} in server: ${guildToSync.name} since they left the main server`); 478 | } 479 | }).catch(e => { 480 | debugLog(`Not removing roles from ${removedMember.displayName} in ${guildToSync.name} because they aren't in that server.`); 481 | }); 482 | } 483 | } 484 | }); 485 | 486 | let debugLog = (str) => { 487 | if (isDebug) { 488 | console.log(str); 489 | } 490 | } 491 | 492 | let throttleUpdate = () => { 493 | setTimeout(() => { 494 | triggeredByIntention = false; 495 | }, 2000); 496 | } 497 | 498 | // Verifies that the user who sent the command has the designated commanderRole from the config file. 499 | let verifyUser = (id, guildId = config.mainServer) => { 500 | return client.guilds.fetch(guildId).then(guild => { 501 | return guild.members.fetch(id).then(member => { 502 | let matchesRoleName = member.roles.cache.find(r => r.name === config.allowedRoleName); 503 | debugLog(`VERIFICATION OF ${member.displayName} IN ${guild.name}`) 504 | debugLog(`Role name matches config: ${!!matchesRoleName}`); 505 | debugLog(`Role id matches config: ${member._roles.includes(config.allowedRoleId)}`); 506 | debugLog(`Is guild owner: ${guild.ownerId === member.id}`); 507 | 508 | return member._roles.includes(config.allowedRoleId) || (guild.ownerId === member.id) || !!matchesRoleName; 509 | }); 510 | }); 511 | } 512 | 513 | // Responds to each (/) slash command with outcome of the command, if this was triggered by a client event or an error, it logs the outcome to the log channel denoted in config 514 | let respondToInteraction = async (interaction, message, error = null) => { 515 | if (!interaction) { 516 | const mainServer = await client.guilds.fetch(config.mainServer); 517 | const logChannel = await mainServer.channels.fetch(config.logChannelId); 518 | logChannel.send(message); 519 | } else { 520 | 521 | let url = `https://discord.com/api/v8/interactions/${interaction.id}/${interaction.token}/callback` 522 | 523 | let json = { 524 | "type": 4, 525 | "data": { 526 | "content": message 527 | } 528 | } 529 | 530 | axios.post(url, json); 531 | } 532 | 533 | if (error) { 534 | console.log(error); 535 | } 536 | 537 | throttleUpdate(); 538 | } 539 | 540 | client.login(config.token); -------------------------------------------------------------------------------- /runReverse.js: -------------------------------------------------------------------------------- 1 | /* 2 | syncBot, a super simple bot that gives you the ability to add/remove a role of a 3 | member in two servers at the same time. 4 | */ 5 | 6 | // Use cases: 7 | // 1. User role is added in a synced server manually, if the user exists in the main server, any role with the same name will be applied in the main server 8 | // 2. User role is added via slash command in a synced server, if the user exists in the main server, the bot will apply the role with the same name in the main server 9 | // 3. User role is removed in a synced server manually, any role with the same name is removed from the user in the main server 10 | // 4. User role is removed via slash command in a synced server, any role with the same name is removed from the user in the main server 11 | // 5. User is added to the main server, roles that match names of roles the user has in any synced server are applied to the user in the main server 12 | // 6. User is removed from a synced server, role names that the user has in the synced server are removed from the main server 13 | import { createRequire } from "module"; 14 | const require = createRequire(import.meta.url); 15 | var config = require("./config.json"); 16 | import { Client, GatewayIntentBits } from "discord.js"; 17 | import { iterateThroughMembers } from "./helpers.js"; 18 | const axios = require("axios"); 19 | const client = new Client({ 20 | intents: [ 21 | GatewayIntentBits.Guilds, 22 | GatewayIntentBits.GuildMembers, 23 | GatewayIntentBits.GuildPresences, 24 | ], 25 | }); 26 | // This is to keep the action from firing twice when using the (/) command, since the guildMemberUpdate will see the role update and fire the add/remove again. 27 | let triggeredByIntention = false; 28 | 29 | client.on("ready", () => { 30 | //TODO: Add validation of the config file 31 | console.log(`syncbot ready!`); 32 | }); 33 | 34 | client.on("interactionCreate", async (interaction) => { 35 | // 2 === APPLICATION_COMMAND 36 | if (interaction.type !== 2) return; 37 | 38 | if (interaction.commandName === "add") { 39 | verifyUser(interaction.member.id).then(async (verified) => { 40 | if (verified) { 41 | let member = interaction.options.getMember("user"); 42 | let role = interaction.options.getRole("role"); 43 | triggeredByIntention = true; 44 | addRole(member, role.id, interaction); 45 | } else { 46 | respondToInteraction( 47 | interaction, 48 | `You dont have the necessary role to send that command ${interaction.user.username}` 49 | ); 50 | } 51 | }); 52 | } 53 | 54 | if (interaction.commandName === "remove") { 55 | verifyUser(interaction.member.id).then(async (verified) => { 56 | if (verified) { 57 | let member = interaction.options.getMember("user"); 58 | let role = interaction.options.getRole("role"); 59 | triggeredByIntention = true; 60 | removeRole(member, role, interaction); 61 | } else { 62 | respondToInteraction( 63 | interaction, 64 | `You dont have the necessary role to send that command ${interaction.user.username}` 65 | ); 66 | } 67 | }); 68 | } 69 | 70 | if (interaction.commandName === "role-checker") { 71 | verifyUser(interaction.member.id).then(async (verified) => { 72 | if (verified) { 73 | let option = interaction.options.data.find( 74 | (obj) => obj.name === "option" 75 | ).value; 76 | triggeredByIntention = true; 77 | if (option === "analyze") { 78 | await iterateThroughMembers( 79 | interaction, 80 | roleAnalyze, 81 | roleAnalyzeCallback 82 | ); 83 | } else if (option === "force") { 84 | await iterateThroughMembers( 85 | interaction, 86 | roleAnalyze, 87 | roleAnalyzeCallback, 88 | true 89 | ); 90 | } 91 | } else { 92 | respondToInteraction( 93 | interaction, 94 | `You dont have the necessary role to send that command ${interaction.user.username}` 95 | ); 96 | } 97 | }); 98 | } 99 | }); 100 | 101 | let roleAnalyze = async (member, interaction, data, forceSync = false) => { 102 | await interaction.deferReply(); 103 | let memberMainserverRolesCollection = member.roles.cache; 104 | let memberMainServerRolesArrayStrings = memberMainserverRolesCollection.map( 105 | (role) => role.name 106 | ); 107 | let memberObj = { 108 | username: member.displayName, 109 | serversWithDifferingRoles: [], 110 | }; 111 | let hasDifferingRoles = false; 112 | 113 | for (const server of config.syncedServers) { 114 | const fetchedServer = await client.guilds.fetch(server); 115 | const fetchedServerRoles = await fetchedServer.roles.fetch(); 116 | if (fetchedServer.ownerId === interaction.member.id) { 117 | let membersInFetchedServer = await fetchedServer.members.fetch(); 118 | let memberInFetchedServer = membersInFetchedServer.get(member.id); 119 | if (memberInFetchedServer) { 120 | let membersRolesInFetchedServer = memberInFetchedServer.roles.cache; 121 | let membersRolesInFetchedServerAsStrings = 122 | membersRolesInFetchedServer.map((role) => role.name); 123 | // Roles that need removed from the user in the fetched server to match the roles the user has in the main server 124 | let rolesCollectionToRemoveInThisServer = 125 | membersRolesInFetchedServer.filter( 126 | (r) => !memberMainServerRolesArrayStrings.includes(r.name) 127 | ); 128 | // Roles that need added to the user in the fetched server to match the roles the user has in the main server 129 | let rolesCollectionToAddInThisServer = memberMainserverRolesCollection 130 | .filter((r) => !membersRolesInFetchedServerAsStrings.includes(r.name)) 131 | // must map the role over to the one in synced server for add 132 | .map( 133 | (role) => 134 | fetchedServerRoles.find((r) => r.name === role.name) || role 135 | ); 136 | 137 | let rolesToRemoveInThisServer = [ 138 | ...rolesCollectionToRemoveInThisServer.values(), 139 | ]; 140 | let rolesToAddInThisServer = [ 141 | ...rolesCollectionToAddInThisServer.values(), 142 | ]; 143 | if ( 144 | rolesToRemoveInThisServer.length > 0 || 145 | rolesToAddInThisServer.length > 0 146 | ) { 147 | hasDifferingRoles = true; 148 | let remove = forceSync 149 | ? "rolesRemovedToMatchMainserver" 150 | : "rolesToRemoveToMatchMainserver"; 151 | let add = forceSync 152 | ? "rolesAddedToMatchMainserver" 153 | : "rolesToAddToMatchMainServer"; 154 | if ( 155 | rolesToRemoveInThisServer.length > 0 && 156 | rolesToAddInThisServer.length === 0 157 | ) { 158 | if (forceSync) { 159 | memberInFetchedServer.roles.remove( 160 | rolesCollectionToRemoveInThisServer 161 | ); 162 | } 163 | 164 | memberObj.serversWithDifferingRoles.push({ 165 | serverName: fetchedServer.name, 166 | [`${remove}`]: rolesToRemoveInThisServer.map((role) => role.name), 167 | }); 168 | } 169 | if ( 170 | rolesToAddInThisServer.length > 0 && 171 | rolesToRemoveInThisServer.length === 0 172 | ) { 173 | if (forceSync) { 174 | memberInFetchedServer.roles.add(rolesCollectionToAddInThisServer); 175 | } 176 | 177 | memberObj.serversWithDifferingRoles.push({ 178 | serverName: fetchedServer.name, 179 | [`${add}`]: rolesToAddInThisServer.map((role) => role.name), 180 | }); 181 | } 182 | if ( 183 | rolesToAddInThisServer.length > 0 && 184 | rolesToRemoveInThisServer.length > 0 185 | ) { 186 | if (forceSync) { 187 | await memberInFetchedServer.roles.remove( 188 | rolesCollectionToRemoveInThisServer 189 | ); 190 | await memberInFetchedServer.roles.add( 191 | rolesCollectionToAddInThisServer 192 | ); 193 | } 194 | 195 | memberObj.serversWithDifferingRoles.push({ 196 | serverName: fetchedServer.name, 197 | [`${remove}`]: rolesToRemoveInThisServer.map((role) => role.name), 198 | [`${add}`]: rolesToAddInThisServer.map((role) => role.name), 199 | }); 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | if (hasDifferingRoles) { 207 | data.membersWithDifferences.push(memberObj); 208 | } 209 | 210 | data.membersAnalyzed++; 211 | 212 | return data; 213 | }; 214 | /** 215 | * 216 | * @param {the interaction from the original command} interaction 217 | * @param {the data procured by running the action on each member} data 218 | * @param {whether we are just analyzing roles, or force syncing} forceSync 219 | */ 220 | let roleAnalyzeCallback = (interaction, data, forceSync) => { 221 | interaction.user 222 | .createDM() 223 | .then((dmChannel) => { 224 | var buf = Buffer.from(JSON.stringify(data, null, 4)); 225 | dmChannel.send({ 226 | files: [ 227 | { 228 | attachment: buf, 229 | name: `${interaction.guild.name}.json`, 230 | }, 231 | ], 232 | }); 233 | }) 234 | .then(async () => { 235 | let analyzed = `I went through and compared roles for ${data.membersAnalyzed} members. I sent you the results in a DM.`; 236 | let forced = `I went through and synced roles for ${data.membersAnalyzed} members. I sent you a report in a DM.`; 237 | return await interaction.editReply({ 238 | content: forceSync ? forced : analyzed, 239 | ephemeral: true, 240 | }); 241 | }); 242 | 243 | throttleUpdate(); 244 | }; 245 | 246 | // Manual function registered to (/) slash command to add a role from a user in the synced server and main server 247 | let addRole = async (member, role, interaction = null) => { 248 | const mainServer = await client.guilds 249 | .fetch(config.mainServer) 250 | .catch((e) => console.log(`ADDROLE_MAINSERVER_FETCH Error: ${e}`)); 251 | const mainServerRoles = await mainServer.roles 252 | .fetch(role) 253 | .catch((e) => console.log(`ADDROLE_MAINSERVER-ROLE_FETCH Error: ${e}`)); 254 | const serverCommandWasInRoleToAdd = await member.guild.roles 255 | .fetch(role) 256 | .catch((e) => console.log(`ADDROLE_SYNCEDSERVER-ROLE_FETCH Error: ${e}`)); 257 | 258 | mainServer.members 259 | .fetch(member.id) 260 | .then((mainServerMember) => { 261 | const mainServerRoleToAdd = mainServerRoles.find( 262 | (r) => r.name === serverCommandWasInRoleToAdd.name 263 | ); 264 | member.roles 265 | .add(serverCommandWasInRoleToAdd) 266 | .then(() => { 267 | mainServerMember.roles 268 | .add(mainServerRoleToAdd) 269 | .then(() => { 270 | respondToInteraction( 271 | interaction, 272 | `Added ${mainServerRoleToAdd.name} to ${mainServerMember.user.username} in ${mainServer.name}` 273 | ); 274 | }) 275 | .catch((err) => 276 | respondToInteraction( 277 | interaction, 278 | `There was an error adding the role in main server: ${mainServer.name}, see console for error`, 279 | err 280 | ) 281 | ); 282 | }) 283 | .catch((err) => 284 | respondToInteraction( 285 | interaction, 286 | "There was an error adding the role in this synced server, see console for error", 287 | err 288 | ) 289 | ); 290 | }) 291 | .catch(() => 292 | respondToInteraction( 293 | interaction, 294 | `Unable to add ${serverCommandWasInRoleToAdd.name} to ${member.user.username} in ${mainServer.name} since the user is not in that server.` 295 | ) 296 | ); 297 | }; 298 | 299 | // Manual function registered to (/) slash command to remove a role from a user in the synced server and main server 300 | let removeRole = async (member, role, interaction = null) => { 301 | const mainServer = await client.guilds 302 | .fetch(config.mainServer) 303 | .catch((e) => console.log(`REMOVEROLE_MAINSERVER_FETCH Error: ${e}`)); 304 | const mainServerRoles = await mainServer.roles 305 | .fetch() 306 | .catch((e) => console.log(`REMOVEROLE_MAINSERVER-ROLES_FETCH Error: ${e}`)); 307 | const serverCommandWasInRoleToRemove = member.roles.resolve(role); 308 | 309 | mainServer.members 310 | .fetch(member.id) 311 | .then((mainServerMember) => { 312 | const mainServerRoleToRemove = mainServerRoles.find( 313 | (r) => r.name === serverCommandWasInRoleToRemove.name 314 | ); 315 | member.roles 316 | .remove(serverCommandWasInRoleToRemove) 317 | .then(() => { 318 | mainServerMember.roles 319 | .remove(mainServerRoleToRemove) 320 | .then(() => { 321 | respondToInteraction( 322 | interaction, 323 | `Removed ${mainServerRoleToRemove.name} from ${mainServerMember.user.username} in ${mainServer.name}` 324 | ); 325 | }) 326 | .catch((err) => 327 | respondToInteraction( 328 | interaction, 329 | `There was an error removing the role in main server: ${mainServer.name}, see console for error`, 330 | err 331 | ) 332 | ); 333 | }) 334 | .catch((err) => 335 | respondToInteraction( 336 | interaction, 337 | "There was an error removing the role in this synced server, see console for error", 338 | err 339 | ) 340 | ); 341 | }) 342 | .catch(() => 343 | respondToInteraction( 344 | interaction, 345 | `Unable to remove ${serverCommandWasInRoleToRemove.name} from ${member.user.username} in ${mainServer.name} since the user is not in that server.` 346 | ) 347 | ); 348 | }; 349 | 350 | let throttleUpdate = () => { 351 | setTimeout(() => { 352 | triggeredByIntention = false; 353 | }, 2000); 354 | }; 355 | 356 | // Verifies that the user who sent the command has the designated commanderRole from the config file. 357 | let verifyUser = (id) => { 358 | return client.guilds 359 | .fetch(config.mainServer) 360 | .then((guild) => { 361 | return guild.members 362 | .fetch(id) 363 | .then((member) => { 364 | return ( 365 | member.roles.cache.find( 366 | (r) => r.name === config.allowedRoleName 367 | ) !== undefined || guild.ownerId === member.id 368 | ); 369 | }) 370 | .catch((err) => `VERIFYUSER_MEMBER_FETCH: ${err}`); 371 | }) 372 | .catch((err) => `VERIFYUSER_CLIENT_GUILDS_FETCH: ${err}`); 373 | }; 374 | 375 | // Responds to each (/) slash command with outcome of the command, if this was triggered by a client event or an error, it logs the outcome to the log channel denoted in config 376 | let respondToInteraction = async (interaction, message, error = null) => { 377 | if (!interaction) { 378 | const mainServer = await client.guilds.fetch(config.mainServer); 379 | const logChannel = await mainServer.channels.fetch(config.logChannelId); 380 | logChannel.send(message); 381 | } else { 382 | let url = `https://discord.com/api/v8/interactions/${interaction.id}/${interaction.token}/callback`; 383 | 384 | let json = { 385 | type: 4, 386 | data: { 387 | content: message, 388 | }, 389 | }; 390 | 391 | axios.post(url, json); 392 | } 393 | 394 | if (error) { 395 | console.log(error); 396 | } 397 | 398 | throttleUpdate(); 399 | }; 400 | 401 | // When a users roles are updated in a synced server, update them in the main server. 402 | client.on("guildMemberUpdate", async (oldMember, newMember) => { 403 | if ( 404 | !triggeredByIntention && 405 | config.syncedServers.includes(newMember.guild.id) 406 | ) { 407 | let oldRoles = oldMember._roles; 408 | let newRoles = newMember._roles; 409 | 410 | if (oldRoles.length > newRoles.length) { 411 | let roleToRemoveId = oldRoles.filter((id) => !newRoles.includes(id))[0]; 412 | removeRole(newMember, oldMember.roles.cache.get(roleToRemoveId)); 413 | } 414 | 415 | if (oldRoles.length < newRoles.length) { 416 | let roleToAddId = newRoles.filter((id) => !oldRoles.includes(id))[0]; 417 | addRole(newMember, roleToAddId); 418 | } 419 | } 420 | }); 421 | 422 | // When a new user joins the main server, then look for that users roles in the synced servers and apply them in the main server. 423 | client.on("guildMemberAdd", async (addedMember) => { 424 | if (config.mainServer === addedMember.guild.id) { 425 | const mainServer = addedMember.guild; 426 | let mainServerMember = addedMember; 427 | let mainServerRoles = await mainServer.roles 428 | .fetch() 429 | .catch((e) => 430 | console.log(`GUILDMEMBERADD_MAINSERVER-ROLES_FETCH Error: ${e}`) 431 | ); 432 | const logChannel = await mainServer.channels 433 | .fetch(config.logChannelId) 434 | .catch((e) => console.log(`GUILDMEMBERADD_LOGCHANNEL_FETCH Error: ${e}`)); 435 | 436 | for (const server of config.syncedServers) { 437 | const guildToSync = await client.guilds 438 | .fetch(server) 439 | .catch((e) => 440 | console.log(`GUILDMEMBERADD_SYNCEDSERVER_FETCH Error: ${e}`) 441 | ); 442 | let memberToSync = await guildToSync.members 443 | .fetch(addedMember.user.id) 444 | .catch((e) => console.log(`GUILDMEMBERADD_MEMBER_FETCH Error: ${e}`)); 445 | if (memberToSync) { 446 | let thisServerRoles = [...memberToSync.roles.cache.values()].filter( 447 | (r) => r.name !== "@everyone" 448 | ); 449 | if (thisServerRoles.length > 0) { 450 | thisServerRoles.forEach((role) => { 451 | let roleToAdd = mainServerRoles.find((r) => r.name === role.name); 452 | if (roleToAdd && roleToAdd.id && roleToAdd.name) { 453 | mainServerMember.roles 454 | .add(roleToAdd) 455 | .catch((e) => 456 | console.log(`GUILDMEMBERADD_ROLE_ADD Error: ${e}`) 457 | ); 458 | } 459 | }); 460 | await logChannel 461 | .send( 462 | `Syncing roles from server: ${guildToSync.name} for new member: ${mainServerMember.user.username}` 463 | ) 464 | .catch((err) => `GUILDMEMBERADD_LOGCHANNEL_SEND: ${err}`); 465 | } 466 | } 467 | } 468 | } 469 | }); 470 | 471 | // When a user leaves a synced server, then remove all of matching roles from the main server. 472 | client.on("guildMemberRemove", async (removedMember) => { 473 | if (config.syncedServers.includes(removedMember.guild.id)) { 474 | const mainServer = await client.guilds 475 | .fetch(config.mainServer) 476 | .catch((err) => `GUILDMEMBERREMOVE-MAINSERVER_FETCH: ${err}`); 477 | const mainServerRoles = await mainServer.roles 478 | .fetch() 479 | .catch((err) => `GUILDMEMBERREMOVE-MAINSERVER_ROLES_FETCH: ${err}`); 480 | const mainServerMember = await mainServer.members 481 | .fetch(removedMember.user.id) 482 | .catch((err) => `GUILDMEMBERREMOVE-MEMBER_FETCH: ${err}`); 483 | const logChannel = await mainServer.channels 484 | .fetch(config.logChannelId) 485 | .catch((err) => `GUILDMEMBERREMOVE-MAINSERVER_LOGCHANNEL_FETCH: ${err}`); 486 | 487 | let syncedServerMemberRoles = removedMember.roles.cache; 488 | 489 | if (mainServerMember) { 490 | for (const [roleId, role] of syncedServerMemberRoles.entries()) { 491 | let roleToRemove = mainServerRoles.find( 492 | (r) => r.name === role.name && r.name !== "@everyone" 493 | ); 494 | 495 | if (roleToRemove) { 496 | await mainServerMember.roles 497 | .remove(roleToRemove) 498 | .catch((err) => `GUILDMEMBERREMOVE_ROLE: ${err}`); 499 | await logChannel 500 | .send( 501 | `Removing roles from: ${mainServerMember.user.username} in server: ${mainServer.name} since they left a synced server: ${removedMember.guild.name}` 502 | ) 503 | .catch((err) => `GUILDMEMBERREMOVE_LOGCHANNEL_SEND: ${err}`); 504 | } 505 | } 506 | } else { 507 | await logChannel 508 | .send( 509 | `Not removing roles from: ${removedMember.username} in mainserver since they aren't in the server.` 510 | ) 511 | .catch( 512 | (err) => `GUILDMEMBERREMOVE-MAINSERVERROLES_LOGCHANNEL_SEND: ${err}` 513 | ); 514 | } 515 | } 516 | }); 517 | 518 | client.login(config.token); 519 | -------------------------------------------------------------------------------- /syncBotLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathonor/syncBot/5eedb5a9dff882d3d60827ca116773d1d2861710/syncBotLogo.png -------------------------------------------------------------------------------- /syncBotLogoBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathonor/syncBot/5eedb5a9dff882d3d60827ca116773d1d2861710/syncBotLogoBlack.png --------------------------------------------------------------------------------