├── .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 |
32 |
33 |
34 |

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