diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36c3dc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv/ +src/settings/config.json \ No newline at end of file diff --git a/TOKEN.txt b/TOKEN.txt deleted file mode 100644 index ee6c122..0000000 --- a/TOKEN.txt +++ /dev/null @@ -1 +0,0 @@ -ENTER THE BOT TOKEN HERE \ No newline at end of file diff --git a/app.py b/app.py deleted file mode 100644 index 6c344ee..0000000 --- a/app.py +++ /dev/null @@ -1,156 +0,0 @@ -# Sync Bot by jjkay03 - -import discord -from discord.ext import commands - -intents = discord.Intents.default(); intents.message_content=True; intents.members=True -client = commands.Bot(command_prefix = "s.", intents=intents) -TOKEN = open("TOKEN.txt", "r").read() -version = "1.0.4" - -# The sync server will copy the roles from the main server -main_server_id = 302087475385539007 # The ID of the main server -sync_server_id = 302087475385539007 # The ID of the sync server - -# This is the role map, ID of a role on the main server and its equivalent on the sync server -# Main server role ID : Sync server role ID -role_id_map = { - 302087475385539007 : 302087475385539007, # Role 1 - 302087475385539007 : 302087475385539007, # Role 2 - 302087475385539007 : 302087475385539007 # Role 3 -} - -# -- EVENTS --------------------------------------------------------------------- - -# On ready -@client.event -async def on_ready(): - await client.change_presence(status=discord.Status.online) - print(">> BOT IS RUNNING") - print(">> Name: {}".format(client.user.name)) - print(">> ID: {}".format(client.user.id)) - print(f">> Version: {version}") - -# Member update -@client.event -async def on_member_update(before, after): - if before.roles != after.roles and before.guild.id == main_server_id: - print(f"[>] Synchronized roles for {before.name} as they updated in the Main Server.") - await sync_roles(before.guild, client.get_guild(sync_server_id), before.id) - -# On member join -@client.event -async def on_member_join(member): - if member.guild.id == sync_server_id: - print(f"[>] Synchronized roles for {member.name} as they joined the Sync Server.") - await sync_roles(client.get_guild(main_server_id), member.guild, member.id) - - -# -- FUNCTIONS ------------------------------------------------------------------ - -async def sync_roles(main_server, sync_server, user_id): - main_member = main_server.get_member(user_id) - sync_member = sync_server.get_member(user_id) - - if main_member is None: - print(f"[D] User with ID {user_id} not found in Main Server.") - if sync_member is None: - print(f"[D] User with ID {user_id} not found in Sync Server."); return - - sync_roles_to_remove = [role for role in sync_member.roles if role.id in role_id_map.values()] - - # Remove sync roles from the user on the sync server - if sync_roles_to_remove: - await sync_member.remove_roles(*sync_roles_to_remove, reason="Removing sync roles.") - print(f"[-] Cleared sync roles the role for {sync_member.name} in the Sync Server.") - - # Return after role clear if not on main server - if main_member is None: - return - - main_user_roles = [role.id for role in main_member.roles] - sync_user_roles = [role.id for role in sync_member.roles] - - # Add roles on the sync server that the user has on the main server - for role_id in main_user_roles: - sync_role_id = role_id_map.get(role_id) - if sync_role_id: - sync_role = sync_server.get_role(sync_role_id) - if sync_role: - await sync_member.add_roles(sync_role, reason="Syncing roles from the main server.") - print(f"[+] Gave {sync_member.name} the role {sync_role.name} in the Sync Server.") - - -# -- COMMANDS ------------------------------------------------------------------- - -# COMMAND: ping -@client.command(aliases=["Ping","PING", "p", "P"], help="Shows you the ping of the Bot") -async def ping(ctx): - ping = round(client.latency * 1000) - await ctx.send(f"**{ping}ms**") - -# COMMAND: sync -@client.command() -async def sync(ctx, user_id: int = None): - if ctx.author.guild_permissions.manage_roles: - main_server = client.get_guild(main_server_id) - sync_server = client.get_guild(sync_server_id) - if main_server is None or sync_server is None: - await ctx.send("One or both of the servers could not be found.") - return - - await sync_roles(main_server, sync_server, user_id) - await ctx.send(":ballot_box_with_check: Roles have been synced successfully!") - - else: - await ctx.message.delete() - await ctx.send(":x: You do not have the required permissions to use this command.", delete_after=5) - -# COMMAND: syncall -@client.command() -async def syncall(ctx): - if ctx.author.guild_permissions.manage_roles: - main_server = client.get_guild(main_server_id) - sync_server = client.get_guild(sync_server_id) - if sync_server is None: - await ctx.send("Sync server not found.") - return - - for member in sync_server.members: - await sync_roles(main_server, sync_server, member.id) - print(f"[!] Synchronized roles for {member.name} in the Sync Server.") - - await ctx.send(":ballot_box_with_check: Roles have been synced for all members on the sync server!") - - else: - await ctx.message.delete() - await ctx.send(":x: You do not have the required permissions to use this command.", delete_after=5) - -# COMMAND: removeall -@client.command() -async def removeall(ctx): - if ctx.author.guild_permissions.manage_roles: - sync_server = client.get_guild(sync_server_id) - - if sync_server is None: - await ctx.send("Sync server not found.") - return - - # Iterate through all members on the sync server - for member in sync_server.members: - sync_roles = [role for role in member.roles if role.id in role_id_map.values()] - if sync_roles: - await member.remove_roles(*sync_roles, reason="Removing all sync roles.") - print(f"[-] Removed sync roles from {member.name} in the Sync Server.") - - await ctx.send(":ballot_box_with_check: Removed all sync roles from everyone on the sync server!") - - else: - await ctx.message.delete() - await ctx.send(":x: You do not have the required permissions to use this command.", delete_after=5) - - -# -- CLIENT RUN ----------------------------------------------------------------- - -# Client Run -client.run(TOKEN) diff --git a/linux_mac-start.sh b/linux_mac-start.sh new file mode 100644 index 0000000..00423a3 --- /dev/null +++ b/linux_mac-start.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Clear terminal +clear +# Load config file +config_file="src/settings/config.json" +token=$(jq -r '.token' "$config_file") +version=$(jq -r '.version' "$config_file") +main_server_id=$(jq -r '.main_server_id' "$config_file") +sync_server_id=$(jq -r '.sync_server_id' "$config_file") + +# Check if all required fields are present +if [ -z "$token" ]; then + echo "Missing required config field: token" + exit 1 +fi +if [ -z "$version" ]; then + echo "Missing required config field: version" + exit 1 +fi +if [ -z "$main_server_id" ]; then + echo "Missing required config field: main_server_id" + exit 1 +fi +if [ -z "$sync_server_id" ]; then + echo "Missing required config field: sync_server_id" + exit 1 +fi + +# Clear terminal +clear + +# Start the bot +python3 src/bot.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c053d86 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +discord.py==2.4.0 \ No newline at end of file diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..641ecb9 --- /dev/null +++ b/src/bot.py @@ -0,0 +1,43 @@ +import sys +import os +import json +import discord +from discord.ext import commands +from src.functions.terminalLog import terminal_log +from src.functions.clearTerminal import clear_terminal +from src.functions.syncRoles import sync_roles +from src.functions.loadConfig import load_config +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +async def main(): + with open("./src/settings/config.json", "r") as config_file: + CONFIG = json.load(config_file) + TOKEN = CONFIG["token"] + VERSION = CONFIG["version"] + MAIN_SERVER_ID = CONFIG["main_server_id"] + SYNC_SERVER_ID = CONFIG["sync_server_id"] + ROLE_ID_MAP = {int(k): int(v) for k, v in CONFIG["role_id_map"].items()} + + intents = discord.Intents.default() + intents.message_content = True + intents.members = True + client = commands.Bot(command_prefix="s.", intents=intents) + + @client.event + async def on_ready(): + await clear_terminal() + await client.change_presence(status=discord.Status.online) + await terminal_log("BOT", f"{client.user.name} version {VERSION} loaded.") + await terminal_log("BOT", f"{client.user.name} ({client.user.id}) is now online.") + + @client.event + async def on_member_update(before, after): + if before.roles != after.roles and before.guild.id == MAIN_SERVER_ID: + await terminal_log("BOT", f"Synchronized roles for {before.name} as they updated in the Main Server.") + await sync_roles(before.guild, client.get_guild(SYNC_SERVER_ID), before.id, ROLE_ID_MAP) + + await client.start(TOKEN) + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) \ No newline at end of file diff --git a/src/functions/__pycache__/clearTerminal.cpython-311.pyc b/src/functions/__pycache__/clearTerminal.cpython-311.pyc new file mode 100644 index 0000000..6c2c4ee Binary files /dev/null and b/src/functions/__pycache__/clearTerminal.cpython-311.pyc differ diff --git a/src/functions/__pycache__/loadConfig.cpython-311.pyc b/src/functions/__pycache__/loadConfig.cpython-311.pyc new file mode 100644 index 0000000..dba4b73 Binary files /dev/null and b/src/functions/__pycache__/loadConfig.cpython-311.pyc differ diff --git a/src/functions/__pycache__/syncRoles.cpython-311.pyc b/src/functions/__pycache__/syncRoles.cpython-311.pyc new file mode 100644 index 0000000..fef8c56 Binary files /dev/null and b/src/functions/__pycache__/syncRoles.cpython-311.pyc differ diff --git a/src/functions/__pycache__/terminalLog.cpython-311.pyc b/src/functions/__pycache__/terminalLog.cpython-311.pyc new file mode 100644 index 0000000..05547ed Binary files /dev/null and b/src/functions/__pycache__/terminalLog.cpython-311.pyc differ diff --git a/src/functions/clearTerminal.py b/src/functions/clearTerminal.py new file mode 100644 index 0000000..0da9d00 --- /dev/null +++ b/src/functions/clearTerminal.py @@ -0,0 +1,37 @@ +import os +from src.functions.terminalLog import terminal_log + +async def clear_terminal(): + """ + Clears the terminal screen. + + This function attempts to clear the terminal screen using the appropriate command + for the operating system. It logs the success or failure of the operation and ensures + that a completion message is logged regardless of the outcome. + + Logs: + SYSTEM: Terminal cleared. + ERROR: Failed to clear terminal: + SYSTEM: clear_terminal function executed. + + Returns: + None + + Raises: + Exception: If an error occurs while attempting to clear the terminal. + + The function performs the following steps: + 1. Attempts to clear the terminal screen using 'cls' for Windows and 'clear' for Unix-based systems. + 2. Logs a message indicating that the terminal has been cleared. + 3. If an error occurs during the clearing process, logs an error message with the exception details. + 4. Logs a message indicating that the function execution is completed, regardless of success or failure. + """ + try: + os.system('cls' if os.name == 'nt' else 'clear') + await terminal_log("SYSTEM", "Terminal cleared.") + except Exception as e: + await terminal_log("ERROR", f"Failed to clear terminal: {e}") + finally: + await terminal_log("SYSTEM", "clear_terminal function executed.") + +__all__ = ['clear_terminal'] \ No newline at end of file diff --git a/src/functions/syncRoles.py b/src/functions/syncRoles.py new file mode 100644 index 0000000..7703aa0 --- /dev/null +++ b/src/functions/syncRoles.py @@ -0,0 +1,76 @@ +import discord +from src.functions.terminalLog import terminal_log + +__all__ = ['sync_roles'] + +async def sync_roles(main_server, sync_server, user_id, role_id_map): + """ + Synchronizes roles for a user between the main server and the sync server. + This function retrieves the user from both the main server and the sync server, + removes any roles in the sync server that are mapped in the role_id_map, and then + adds roles to the user in the sync server based on their roles in the main server. + Args: + main_server (discord.Guild): The main server object where the user's roles are sourced. + sync_server (discord.Guild): The sync server object where the user's roles are synchronized. + user_id (int): The ID of the user whose roles are being synchronized. + role_id_map (dict): A dictionary mapping role IDs from the main server to the sync server. + Returns: + None + Logs: + "DEBUG": If the user is not found in either the main server or the sync server. + "BOT": When roles are successfully cleared or added in the sync server. + "ERROR": If there are missing permissions to remove or add roles, or if any other error occurs during synchronization. + "DEBUG": When the role synchronization process is completed. + Raises: + Exception: If any error occurs during the role synchronization process. + The function performs the following steps: + 1. Retrieves the user from both the main server and the sync server. + 2. Logs a debug message if the user is not found in either server. + 3. Identifies roles in the sync server that need to be removed based on the role_id_map. + 4. Attempts to remove the identified roles from the user in the sync server. + 5. Logs a message if roles are successfully removed or if there are missing permissions. + 6. If the user is found in the main server, retrieves their roles. + 7. Maps the user's roles from the main server to the sync server using the role_id_map. + 8. Attempts to add the mapped roles to the user in the sync server. + 9. Logs a message if roles are successfully added or if there are missing permissions. + 10. Logs a debug message when the role synchronization process is completed. + """ + try: + main_member = main_server.get_member(user_id) + sync_member = sync_server.get_member(user_id) + + if main_member is None: + await terminal_log("DEBUG", f"User with ID {user_id} not found in Main Server.") + if sync_member is None: + await terminal_log("DEBUG", f"User with ID {user_id} not found in Sync Server.") + return + + sync_roles_to_remove = [role for role in sync_member.roles if role.id in role_id_map.values()] + + if sync_roles_to_remove: + try: + await sync_member.remove_roles(*sync_roles_to_remove, reason="Removing sync roles.") + await terminal_log("BOT", f"Cleared sync roles for {sync_member.name} in the Sync Server.") + except discord.errors.Forbidden: + await terminal_log("ERROR", f"Missing permissions to remove roles from {sync_member.name} in the Sync Server.") + + if main_member is None: + return + + main_user_roles = [role.id for role in main_member.roles] + sync_user_roles = [role.id for role in sync_member.roles] + + for role_id in main_user_roles: + sync_role_id = role_id_map.get(role_id) + if sync_role_id: + sync_role = sync_server.get_role(sync_role_id) + if sync_role: + try: + await sync_member.add_roles(sync_role, reason="Syncing roles from the main server.") + await terminal_log("BOT", f"Gave {sync_member.name} the role {sync_role.name} in the Sync Server.") + except discord.errors.Forbidden: + await terminal_log("ERROR", f"Missing permissions to add roles to {sync_member.name} in the Sync Server.") + except Exception as e: + await terminal_log("ERROR", f"An error occurred during role synchronization: {e}") + finally: + await terminal_log("DEBUG", "Role synchronization process completed.") diff --git a/src/functions/terminalLog.py b/src/functions/terminalLog.py new file mode 100644 index 0000000..97ede2c --- /dev/null +++ b/src/functions/terminalLog.py @@ -0,0 +1,38 @@ +import logging + +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] %(message)s', + datefmt='%H.%M.%d' +) + +__all__ = ['terminal_log'] + +async def terminal_log(prefix, message): + """ + Logs a message to the terminal and handles any exceptions that occur during the logging process. + + This function attempts to log a message with a given prefix using the logging module. If an exception occurs + during the logging process, it catches the exception and prints an error message to the terminal. Regardless + of whether an exception occurs, it ensures that a completion message is printed to the terminal. + + Args: + prefix (str): A string prefix to categorize the log message (e.g., "ERROR", "SYSTEM", "BOT"). + message (str): The message to be logged. + + Logs: + INFO: Logs the message with the given prefix. + ERROR: If an exception occurs, logs an error message with the exception details. + SYSTEM: Logs a completion message indicating that the function execution is completed. + + Exceptions: + Exception: Catches any exception that occurs during the logging process and prints an error message. + + The function performs the following steps: + 1. Attempts to log the message with the given prefix using the logging module. + 2. If an exception occurs, catches the exception and prints an error message with the exception details. + """ + try: + logging.info(f'[{prefix}] {message}') + except Exception as e: + print(f'[{prefix}] An error occurred: {e}') \ No newline at end of file diff --git a/src/settings/template-config.json b/src/settings/template-config.json new file mode 100644 index 0000000..ff1f4ec --- /dev/null +++ b/src/settings/template-config.json @@ -0,0 +1,10 @@ +{ + "token": "", + "version": "", + "main_server_id": "", + "sync_server_id": "", + "role_id_map": { + "ROLEID1MAIN": "ROLEIDSYNC", + "ROLEID2MAIN": "ROLEIDSYNC" + } +} \ No newline at end of file diff --git a/windows-start.bat b/windows-start.bat new file mode 100644 index 0000000..5962c5d --- /dev/null +++ b/windows-start.bat @@ -0,0 +1,34 @@ +@echo off +setlocal + +REM Load config file +set "config_file=src\settings\config.json" +for /f "tokens=2 delims=:," %%A in ('findstr /i "token" "%config_file%"') do set "token=%%~A" +for /f "tokens=2 delims=:," %%A in ('findstr /i "version" "%config_file%"') do set "version=%%~A" +for /f "tokens=2 delims=:," %%A in ('findstr /i "main_server_id" "%config_file%"') do set "main_server_id=%%~A" +for /f "tokens=2 delims=:," %%A in ('findstr /i "sync_server_id" "%config_file%"') do set "sync_server_id=%%~A" + +REM Check if all required fields are present +if "%token%"=="" ( + echo Missing required config field: token + exit /b 1 +) +if "%version%"=="" ( + echo Missing required config field: version + exit /b 1 +) +if "%main_server_id%"=="" ( + echo Missing required config field: main_server_id + exit /b 1 +) +if "%sync_server_id%"=="" ( + echo Missing required config field: sync_server_id + exit /b 1 +) + +REM Clear terminal +cls + +REM Start the bot +python src\bot.py +endlocal \ No newline at end of file