diff --git a/autowl/Cogs/Group.py b/autowl/Cogs/Group.py new file mode 100644 index 0000000..735d98d --- /dev/null +++ b/autowl/Cogs/Group.py @@ -0,0 +1,118 @@ +import discord +from autowl import config +from autowl.bot import Bot +from discord.ext import commands +from discord import app_commands + + +class Group(commands.Cog, name="group"): + def __init__(self, client: Bot): + self.client = client + + @app_commands.command() + async def add( + self, + interaction: discord.Interaction, + role: discord.Role, + ): + + if self.client.whitelist.get(role.name): + await interaction.response.send_message( + f"**{role.name}** is already added, cannot add it again!" + ) + return + + dropdown = discord.ui.Select( + min_values=1, + max_values=20, + placeholder="Choose group permissions in Squad", + options=[ + discord.SelectOption( + label="changemap", description="Allows users to change the map" + ), + discord.SelectOption( + label="pause", description="Pause server gameplay" + ), + discord.SelectOption( + label="cheat", description="Use server cheat commands" + ), + discord.SelectOption( + label="private", description="Password protect server" + ), + discord.SelectOption( + label="balance", description="Group Ignores server team balance" + ), + discord.SelectOption( + label="chat", description="Admin chat and Server broadcast" + ), + discord.SelectOption( + label="kick", + description="Allows user to kick players from the server", + ), + discord.SelectOption( + label="ban", + description="Allows user to ban players from the server", + ), + discord.SelectOption( + label="config", description="Change server config" + ), + discord.SelectOption( + label="cameraman", description="Admin spectate mode" + ), + discord.SelectOption( + label="immune", description="Cannot be kicked / banned" + ), + discord.SelectOption( + label="manageserver", description="Shutdown server" + ), + discord.SelectOption( + label="featuretest", + description="Any features added for testing by dev team", + ), + discord.SelectOption(label="reserve", description="Reserve slot"), + discord.SelectOption( + label="demos", + description="Record Demos on the server side via admin commands", + ), + discord.SelectOption( + label="clientdemos", + description="Record Demos on the client side via commands or the replay UI.", + ), + discord.SelectOption( + label="debug", + description="show admin stats command and other debugging info", + ), + discord.SelectOption( + label="teamchange", description="No timer limits on team change" + ), + discord.SelectOption( + label="forceteamchange", + description="Can issue the ForceTeamChange command", + ), + discord.SelectOption( + label="canseeadminchat", + description="This group can see the admin chat and teamkill/admin-join notifications", + ), + ], + ) + view = discord.ui.View() + view.add_item(dropdown) + + async def perms_handler(interaction: discord.Interaction): + perms = "\n - ".join(dropdown.values) + dropdown.disabled = True + await interaction.response.edit_message(view=view) + await interaction.followup.send( + f"Adding **{role.name}** to whitelist with permissions:\n - {perms}\n\n" + ) + self.client.whitelist[f"{role.name}"] = config.WhitelistGroup( + permissions=dropdown.values, discord_role_id=role.id, members={} + ) + print(self.client.whitelist) + + dropdown.callback = perms_handler + + ctx: commands.Context = await self.client.get_context(interaction) + + + await ctx.send(view=view) diff --git a/autowl/Cogs/Whitelist.py b/autowl/Cogs/Whitelist.py new file mode 100644 index 0000000..fef3da3 --- /dev/null +++ b/autowl/Cogs/Whitelist.py @@ -0,0 +1,27 @@ +import discord +from autowl import config +from autowl.bot import Bot +from discord.ext import commands +from discord import app_commands + + +class Whitelist(commands.Cog): + def __init__(self, client: Bot): + self.client = client + + @app_commands.command() + async def register(self, interaction: discord.Interaction, steam64: int): + ctx: commands.Context = await self.client.get_context(interaction) + if not ctx.guild: + ctx.reply("This command must be ran within a discord server!") + return + + steam64_updated = False + for role in ctx.author.roles: + for group in self.client.whitelist: + if role.id == group.discord_role_id: + steam64_updated = True + group.members[ctx.author.id] = config.WhitelistMember(ctx.author.name, steam64) + + if steam64_updated: + ctx.reply(f"Updated {ctx.author.name}'s whitelist steam64 to {steam64}!") diff --git a/autowl/Cogs/__init__.py b/autowl/Cogs/__init__.py new file mode 100644 index 0000000..34d971a --- /dev/null +++ b/autowl/Cogs/__init__.py @@ -0,0 +1,4 @@ +from .Whitelist import Whitelist +from .Group import Group + +__all__ = ["Whitelist", "Group"] diff --git a/autowl/__init__.py b/autowl/__init__.py index b4ebd70..e69de29 100644 --- a/autowl/__init__.py +++ b/autowl/__init__.py @@ -1,19 +0,0 @@ -import os -from sys import stderr -import discord - - -# run init stuff inside this function -def init(): - # make sure this prints the discord token - # set this up as a runtime environment variable, DO NOT HARDCODE THE TOKEN - distoken = os.environ.get("DISCORD_TOKEN") - if not distoken: - print("Unable to find discord token in environment!", file=stderr) - exit(1) - - - print(f"discord token:{distoken}") - - -init() diff --git a/autowl/__main__.py b/autowl/__main__.py index fac6126..55c46e6 100644 --- a/autowl/__main__.py +++ b/autowl/__main__.py @@ -1,28 +1,65 @@ +import logging +import sys +import autowl.bot as bot from os import environ -from sys import stderr -from multiprocessing import Process -import autowl.fileServer as fileServer -import autowl.discordBot as discordBot +from autowl.config import DiscordClientConfig + +log = logging.getLogger(__name__) + + +class CustomFormat(logging.Formatter): + grey = "\x1b[38;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + reset = "\x1b[0m" + format = ( + "[%(asctime)s][%(levelname)s][%(name)s.%(funcName)s:%(lineno)d] %(message)s" + ) + + FORMATS = { + logging.DEBUG: grey + format + reset, + logging.INFO: grey + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset, + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +def setup_logging(): + log_level = environ.get("LOG_LEVEL") or "INFO" + + ch = logging.StreamHandler(stream=sys.stdout) + ch.setFormatter(CustomFormat()) + + log.setLevel(log_level) + discord_log = logging.getLogger("discord") + discord_log.setLevel(log_level) + discord_http_log = logging.getLogger("discord.http") + discord_http_log.setLevel(log_level) + + logging.basicConfig(level=log_level, handlers=[ch]) -# main runtime function def main(): - if disToken := environ.get('DISCORD_TOKEN'): - print(f"Received discord token: {disToken}") - # Call into main action here, basically launch the bot via import to a lower module + setup_logging() + + if disToken := environ.get("DISCORD_TOKEN"): + bot_config = DiscordClientConfig(disToken) + try: + bot.Bot(bot_config).start_bot() + except Exception as e: + log.critical(f"Bot exited critically, error: {e}") + raise e else: - print("Unable to access DISCORD_TOKEN in environment!", file=stderr) + log.error("Unable to access DISCORD_TOKEN in environment!") exit(1) - fsproc = Process(target=fileServer.startServer) - disbot = Process(target=discordBot.startBot, args=(disToken,)) - - fsproc.start() - disbot.start() - - fsproc.join() - disbot.join() - exit(0) diff --git a/autowl/bot.py b/autowl/bot.py new file mode 100644 index 0000000..c047dfa --- /dev/null +++ b/autowl/bot.py @@ -0,0 +1,52 @@ +import logging +import discord +from discord.ext import commands +from autowl import config + +log = logging.getLogger(__name__) + + +class Bot(commands.Bot): + whitelist = config.Whitelist({}).whitelist + + def __init__(self, config: config.DiscordClientConfig): + self.config = config + intents = discord.Intents.default() + intents.message_content = True + intents.members = True + super().__init__( + command_prefix=commands.when_mentioned_or("&"), + intents=intents, + help_command=commands.DefaultHelpCommand(dm_help=True), + ) + + async def on_ready(self): + log.info(f"Logged in as '{self.user}' ({self.user.id})") + log.info( + f"Have access to the following guilds: " + f"{', '.join([str(guild.name) + ' (' + str(guild.id) + ')' for guild in self.guilds])}" + ) + + for guild in self.guilds: + self.tree.copy_global_to(guild=guild) + await self.tree.sync(guild=guild) + log.info(f"Synced guild: {guild.name}") + + await self.tree.sync() + + async def setup_hook(self): + log.info("Setting up bot") + from autowl import Cogs + + await self.add_cog(Cogs.Whitelist(self)) + await self.add_cog(Cogs.Group(self)) + + def start_bot(self): + log.info("Starting discord bot") + + try: + self.run(self.config.login_token, log_handler=None) + except discord.errors.LoginFailure as e: + log.debug(f"Received login failure: {e}") + log.error("Failed to login to discord, check your discord token!") + raise e diff --git a/autowl/config.py b/autowl/config.py new file mode 100644 index 0000000..73e651e --- /dev/null +++ b/autowl/config.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass + + +@dataclass +class DiscordClientConfig: + login_token: str + + +@dataclass +class WhitelistMember: + discord_username: str + steam64: int + + +@dataclass +class WhitelistGroup: + permissions: list[str] + discord_role_id: int + members: dict[int, WhitelistMember] + + +@dataclass +class Whitelist: + whitelist: dict[str, WhitelistGroup] + + def __iter__(self): + for key in self.whitelist: + yield self.whitelist[key] + + +@dataclass +class PermGroup: + group_name: str + permissions: list[str] diff --git a/autowl/discordBot.py b/autowl/discordBot.py deleted file mode 100644 index f56290c..0000000 --- a/autowl/discordBot.py +++ /dev/null @@ -1,105 +0,0 @@ -import discord -from sys import stderr - -MY_GUILD = discord.Object(id=1064037361786302475) -adminsroleids = [1067044106439761950] -generalwl = dict(name='Whitelist', discordid=1067044106439761950, permslist=['reserve'], members=[]) -masterwllist = [generalwl] - - -def removewlmember(wllist, userid): - memberrmindex = -1 - for i in range(len(wllist['members'])): - if wllist['members'][i]['userid'] == userid: - memberrmindex = i - if memberrmindex < 0: - return - wllist['members'].remove(wllist['members'][memberrmindex]) - - -async def updatefile(wllist): - with open(f"remoteAdmins/{wllist['name']}", 'w') as file: - rawperm = '' - for perm in wllist['permslist']: - rawperm = f"{rawperm},{perm}" - file.write(f"Group={wllist['name']}:{rawperm}\n\n") - for wluser in wllist['members']: - file.write(f"Admin={wluser['steamid']}:{wllist['name']} // discord:{wluser['username']} ({wluser['userid']})\n") - - -class WhiteLister(discord.Client): - def __init__(self, *, intents: discord.Intents): - super().__init__(intents=intents) - self.tree = discord.app_commands.CommandTree(self) - - async def on_ready(self): - print(f"Logged on as {self.user}") - - async def on_member_update(self, before, after): - print(f"user '{after.name} ({after.id})' started member update") - rmrolesid = [] - for befrole in before.roles: - rmrolesid.append(befrole.id) - for aftrole in after.roles: - for befrole in before.roles: - if befrole.id == aftrole.id: - rmrolesid.remove(aftrole.id) - - for rmroleid in rmrolesid: - for wllist in masterwllist: - if wllist['discordid'] == rmroleid: - removewlmember(wllist, before.id) - await updatefile(wllist) - - async def setup_hook(self): - self.tree.copy_global_to(guild=MY_GUILD) - await self.tree.sync(guild=MY_GUILD) - - -async def hellofunc(interaction: discord.Interaction, steam64id: str): - print(f"user '{interaction.user.name} ({interaction.user.id})' attempting whitelist") - hasrole = False - for userrole in interaction.user.roles: - if userrole.id == WHITELISTID: - hasrole = True - if not hasrole: - print(f"user '{interaction.user.name} ({interaction.user.id})' does not have whitelist role") - await interaction.response.send_message(f"ERROR: user does not have whitelist role!") - return - for wlentry in generalwhitelist: - if wlentry[0] == steam64id: - print(f"user '{interaction.user.name} ({interaction.user.id})' used an existing steam id") - await interaction.response.send_message(f"ERROR: steam64id already exists in whitelist!") - return - if wlentry[1] == interaction.user.id: - print(f"user '{interaction.user.name} ({interaction.user.id})' used an existing discord id") - await interaction.response.send_message(f"ERROR: discord user id already exists in whitelist!") - return - username = interaction.user.name - if not (interaction.user.nick is None): - username = interaction.user.nick - newwlentry = dict(steamid=steam64id, userid=interaction.user.id, username=username) - generalwl["members"].append(newwlentry) - print(f"user '{interaction.user.name} ({interaction.user.id})' added with steamid '{steam64id}'") - await updateFile() - await interaction.response.send_message( - f'Added {newwlentry[2]} ({newwlentry[1]}) with steamid: {newwlentry[0]}, to glbwhitelist!') - -def startBot(discordtoken): - print("starting discord bot") - intents = discord.Intents.default() - intents.message_content = True - intents.members = True - - client = WhiteLister(intents=intents) - - @client.tree.command() - async def hello(interaction: discord.Interaction, steam64id: str): - await hellofunc(interaction, steam64id) - - try: - print("discord bot started!") - client.run(discordtoken) - except: - print("Invalid discord token!", file=stderr) - exit(1) diff --git a/autowl/fileServer.py b/autowl/fileServer.py deleted file mode 100644 index dd22696..0000000 --- a/autowl/fileServer.py +++ /dev/null @@ -1,34 +0,0 @@ -import http.server -import socketserver -from urllib.parse import urlparse -from urllib.parse import parse_qs -from sys import stderr - -PORT = 8000 - - -class serveRA(http.server.SimpleHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header("Content-type", "text") - self.end_headers() - query = parse_qs(urlparse(self.path).query) - if 'serverName' in query: - servername = query['serverName'][0] - try: - file = open(f"remoteAdmins/{servername}.cfg", 'rb') - except: - print("failed to open file!", file=stderr) - return - - self.copyfile(file, self.wfile) - return - - -def startServer(): - handler = serveRA - - with socketserver.TCPServer(("", PORT), handler) as httpd: - print(f"Server started at localhost:{PORT}") - httpd.serve_forever() - diff --git a/docs/example.json b/docs/example.json index 9c0f9d1..4882c3e 100644 --- a/docs/example.json +++ b/docs/example.json @@ -2,12 +2,11 @@ "WhiteListGroupName": { "permissions": [], "discord_role_id": "", - "members": [ - { - "discord_id": "", + "members": { + "discord_id": { "steam64": "", "discord_username": "" } - ] + } } } diff --git a/remoteAdmins/vanilla.cfg b/remoteAdmins/vanilla.cfg deleted file mode 100644 index 71698d9..0000000 --- a/remoteAdmins/vanilla.cfg +++ /dev/null @@ -1,101 +0,0 @@ -Group=1039155511066636329: -Group=Odin:changemap,pause,balance,chat,kick,ban,cameraman,teamchange,forceteamchange,canseeadminchat,reserve,config -Group=SrAdmin:changemap,balance,chat,kick,ban,cameraman,teamchange,forceteamchange,canseeadminchat,reserve -Group=JrAdmin:balance,chat,kick,ban,teamchange,canseeadminchat,reserve -Group=Whitelist:reserve -Group=CAS:reserve -Group=HC:reserve -Group=SLT:reserve -Group=BR1:reserve - -///////////////////////////////////////////////////////////////////////////////////////////// -// The format for adding admins is: -// Admin=: -// -// For example: -// Admin=123456:Admin // Adam the admin -// Admin=654321:Moderator // Molly the moderator -// -// Add your own below: -///////////////////////////////////////////////////////////////////////////////////////////// - -// === 1039155511066636329 === - -// === Odin === -Admin=76561197986710399:Odin // Kinsher - Server Administrator -Admin=76561199101367413:Odin // Atrocity - -Admin=76561198001193856:Odin // Xikky - -Admin=76561199028579724:Odin // iiTherRealMcCoy - -Admin=76561198153769543:Odin // MaxRecon - -Admin=76561198067016254:Odin // SheHulk - -Admin=76561198404402665:Odin // Malinoff - -Admin=76561198073942737:Odin // Someoneulove - -Admin=76561198356666755:Odin // Teggy - -Admin=76561198855097026:Odin // SooperGloo - - -// === SrAdmin === -Admin=76561198339174737:SrAdmin // Frizz - -Admin=76561198373523022:SrAdmin // Lt_Longsword - -Admin=76561199201845769:SrAdmin // Buddy - -Admin=76561198015328506:SrAdmin // Ray - -Admin=76561198329170534:SrAdmin // Freeman - -Admin=76561199018771290:SrAdmin // Omega - -Admin=76561198046579272:SrAdmin // Coiffee - -Admin=76561198203568744:SrAdmin // Elon - -Admin=76561198799111045:SrAdmin // Kin_Seward - -Admin=76561198206734757:SrAdmin // RangerSix - -Admin=76561199094282718:SrAdmin // Epimetheus - -Admin=76561199127559778:SrAdmin // Chemji - -Admin=76561198059250541:SrAdmin // Mirage40K - -Admin=76561198081576045:SrAdmin // Rabbid_Squirrel - -Admin=76561198124658412:SrAdmin // Sweetwater - -Admin=76561198107367726:SrAdmin // wolf.rayne - - -// === JrAdmin === -Admin=76561198003130199:JrAdmin // Sexo - Primary Seeder - -// === Whitelist === -Admin=76561198090760895:Whitelist // Treay ~ Sam - For Asgard IT Started 01/09/2023 -Admin=76561198106004468:Whitelist // Trippy Chivas - devbryanbar@gmail.com -Admin=76561198209119887:Whitelist // RIOTYouth1 - aclerc1220@gmail.com -Admin=76561198312514319:Whitelist // {ASG} Scarface - logangrimes11@gmail.com -Admin=76561198983371735:Whitelist // {ASG} young sloth - logangrimes11@gmail.com -Admin=76561198221715432:Whitelist // Skillet - Server Administrator -Admin=76561198026192477:Whitelist // LtJamesFox - Nevetsu@hotmail.com -Admin=76561198059124010:Whitelist // tcandan88(4skin) - ducati979@aol.com -Admin=76561199062085282:Whitelist // Dick Deflator - Granted Whitelist for 3 month's started 12/01/2022/Ends 02/28/2023 -Admin=76561198059462064:Whitelist // Bagels - adroessler10@gmail.com -Admin=76561198119385943:Whitelist // Hypothermiack - -Admin=76561199123384670:Whitelist // TapatioTimmy - camdenricketts4@gmail.com -Admin=76561199285813056:Whitelist // TheGoochSlooth - justintestorelli@gmail.com -Admin=76561197963075020:Whitelist // Subterfuge - jwlind@gmail.com -Admin=76561198128034210:Whitelist // Kybar - carbarykyle@gmail.com -Admin=76561198049739312:Whitelist // Juggernaut - robledoaustin@yahoo.com -Admin=76561197979663830:Whitelist // DamiSupreme - alexdemiane93@gmail.com -Admin=76561199241025555:Whitelist // Dang Li Wang - n.hinesly0602@gmail.com -Admin=76561199241627472:Whitelist // Jenna Tools - n.hinesly0602@gmail.com -Admin=76561199302780874:Whitelist // Mr_poopy_diaper - Attached to (ASG) Kybar (carbarykyle@gmail.com) -Admin=76561198801895406:Whitelist // OneBarStatus - jacobadams135@yahoo.com - -// === CAS === -Admin=7656119844551138:CAS // Albertime - CAS -Admin=76561198094918611:CAS // JF - CAS -Admin=76561198153409985:CAS // Jashy - CAS -Admin=76561198150626482:CAS // Oscar - CAS -Admin=76561198248146940:CAS // Skay - CAS - -// === HC === -Admin=76561198043032389:HC // Sir Peabody - HC -Admin=76561197982328479:HC // Kameron - HC -Admin=76561198101099268:HC // Evans - HC -Admin=76561198249614886:HC // Brodizle - HC -Admin=76561198191984002:HC // Skeebler - HC - -// === SLT === -Admin=76561198149412908:SLT // EnderDevs - SLT -Admin=76561198448114730:SLT // JoeBrandonCheated - SLT - -// === BR1 === -Admin=76561199060693600:BR1 // Madtopher - BR1 -Admin=76561198037486479:BR1 // Dr. Hammerstein - BR1 -Admin=76561198315718944:BR1 // Bags - BR1