diff options
-rwxr-xr-x | .gitignore | 11 | ||||
-rwxr-xr-x | bot.py | 8 | ||||
-rwxr-xr-x | cogs/admin.py | 17 | ||||
-rwxr-xr-x | cogs/logs.py | 18 | ||||
-rwxr-xr-x | cogs/src.py | 134 | ||||
-rwxr-xr-x | cogs/utils.py | 10 | ||||
-rwxr-xr-x | main.py | 64 | ||||
-rw-r--r-- | readme.md | 17 |
8 files changed, 181 insertions, 98 deletions
@@ -1,8 +1,9 @@ -__pycache__ +.vscode/ +__pycache__/ +downloads/ +api_keys.json config.json +blacklist.json +video_blacklist.json discord.log -api_keys.json -.vscode/ leaderboard.png -downloads/ -blacklist.json
\ No newline at end of file @@ -33,7 +33,7 @@ def get_prefix(bot, message): class BedrockBot(commands.Bot): def __init__(self): - super().__init__(command_prefix=get_prefix, case_insensitive=True) + super().__init__(command_prefix=get_prefix, case_insensitive=True, allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=False)) self.logger = logging.getLogger('discord') self.messageBlacklist = [] self.session = aiohttp.ClientSession() @@ -60,6 +60,12 @@ class BedrockBot(commands.Bot): except json.decoder.JSONDecodeError: self.blacklist = [] + with open('video_blacklist.json', 'r') as f: + try: + self.video_blacklist = json.load(f) + except json.decoder.JSONDecodeError: + self.video_blacklist = [] + self.logger.warning(f'Online: {self.user} (ID: {self.user.id})') async def on_message(self, message): diff --git a/cogs/admin.py b/cogs/admin.py index 3a6299f..8164790 100755 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -237,6 +237,23 @@ class Admin(commands.Cog): """Print a config variable, use for testing""" await ctx.send(self.bot.config[str(ctx.message.guild.id)][key]) + @commands.command() + @commands.check(is_mod) + async def blacklistvideo(self, ctx, uri): + """Set runs from a specific url to be auto rejected""" + with open('video_blacklist.json', 'w') as f: + self.bot.video_blacklist.append(uri) + json.dump(self.bot.video_blacklist, f, indent=4) + await ctx.send(f'Blacklisted runs from `{uri}`') + + @commands.command() + @commands.check(is_mod) + async def video_blacklist(self, ctx): + """Sends a list of blacklisted uris""" + message = '```The following URIs are blacklisted:\n' + for uri in self.bot.video_blacklist: + message += f'{uri}, ' + await ctx.send(f'{message[:-2]}```') def setup(bot): bot.add_cog(Admin(bot)) diff --git a/cogs/logs.py b/cogs/logs.py index c1b3a49..4f6aab1 100755 --- a/cogs/logs.py +++ b/cogs/logs.py @@ -19,15 +19,15 @@ class Logs(commands.Cog): color = message.author.color channel = self.bot.get_channel(int(self.bot.config[str(message.guild.id)]["logs_channel"])) embed = discord.Embed( - title='Deleted Message', + title='**Deleted Message**', color=color, timestamp=message.created_at ) embed.add_field( - name='User', value=message.author.mention, inline=True) + name='**User**', value=message.author.mention, inline=True) embed.add_field( - name='Channel', value=message.channel.mention, inline=True) - embed.add_field(name='Message', value=message.content, inline=False) + name='**Channel**', value=message.channel.mention, inline=True) + embed.add_field(name='**Message**', value=message.content, inline=False) await channel.send(embed=embed) @commands.Cog.listener() @@ -43,17 +43,17 @@ class Logs(commands.Cog): else: color = before.author.color embed = discord.Embed( - title='Edited Message', + title='**Edited Message**', color=color, timestamp=after.edited_at ) embed.add_field( - name='User', value=before.author.mention, inline=True) + name='**User**', value=before.author.mention, inline=True) embed.add_field( - name='Channel', value=before.channel.mention, inline=True) - embed.add_field(name='Original Message', + name='**Channel**', value=before.channel.mention, inline=True) + embed.add_field(name='**Original Message**', value=before.content, inline=False) - embed.add_field(name='New Message', value=after.content, inline=False) + embed.add_field(name='**New Message**', value=after.content, inline=False) await channel.send(embed=embed) diff --git a/cogs/src.py b/cogs/src.py index bf776e7..8afe1b5 100755 --- a/cogs/src.py +++ b/cogs/src.py @@ -9,84 +9,90 @@ import asyncio import dateutil.parser from pathlib import Path + async def rejectRun(self, apiKey, ctx, run, reason): await ctx.message.delete() run = run.split('/')[-1] - reject = { - "status": { - "status": "rejected", - "reason": reason - } - } - r = requests.put(f"https://www.speedrun.com/api/v1/runs/{run}/status", headers={ - "X-API-Key": apiKey, "Accept": "application/json", "User-Agent": "mcbeDiscordBot/1.0"}, data=json.dumps(reject)) + reject = {"status": {"status": "rejected", "reason": reason}} + r = requests.put(f"https://www.speedrun.com/api/v1/runs/{run}/status", + headers={ + "X-API-Key": apiKey, + "Accept": "application/json", + "User-Agent": "mcbeDiscordBot/1.0" + }, + data=json.dumps(reject)) if r.status_code == 200 or r.status_code == 204: await ctx.send("Run rejected succesfully") else: await ctx.send("Something went wrong") - await ctx.message.author.send(f"```json\n{json.dumps(json.loads(r.text),indent=4)}```") + await ctx.message.author.send( + f"```json\n{json.dumps(json.loads(r.text),indent=4)}```") async def approveRun(self, apiKey, ctx, run, reason=None): await ctx.message.delete() run = run.split('/')[-1] if reason == None: - approve = { - "status": { - "status": "verified" - } - } + approve = {"status": {"status": "verified"}} else: - approve = { - "status": { - "status": "verified", - "reason":reason - } - } - r = requests.put(f"https://www.speedrun.com/api/v1/runs/{run}/status", headers={ - "X-API-Key": apiKey, "Accept": "application/json", "User-Agent": "mcbeDiscordBot/1.0"}, data=json.dumps(approve)) + approve = {"status": {"status": "verified", "reason": reason}} + r = requests.put(f"https://www.speedrun.com/api/v1/runs/{run}/status", + headers={ + "X-API-Key": apiKey, + "Accept": "application/json", + "User-Agent": "mcbeDiscordBot/1.0" + }, + data=json.dumps(approve)) if r.status_code == 200 or r.status_code == 204: await ctx.send("Run approved succesfully") else: await ctx.send("Something went wrong") - await ctx.message.author.send(f"```json\n{json.dumps(json.loads(r.text),indent=4)}```") + await ctx.message.author.send( + f"```json\n{json.dumps(json.loads(r.text),indent=4)}```") async def deleteRun(self, apiKey, ctx, run): await ctx.message.delete() run = run.split('/')[-1] - r = requests.delete(f"https://www.speedrun.com/api/v1/runs/{run}", headers={ - "X-API-Key": apiKey, "Accept": "application/json", "User-Agent": "mcbeDiscordBot/1.0"}) + r = requests.delete(f"https://www.speedrun.com/api/v1/runs/{run}", + headers={ + "X-API-Key": apiKey, + "Accept": "application/json", + "User-Agent": "mcbeDiscordBot/1.0" + }) if r.status_code == 200 or r.status_code == 204: await ctx.send("Run deleted succesfully") else: await ctx.send("Something went wrong") - await ctx.message.author.send(f"```json\n{json.dumps(json.loads(r.text),indent=4)}```") + await ctx.message.author.send( + f"```json\n{json.dumps(json.loads(r.text),indent=4)}```") + async def pendingRuns(self, ctx): mcbe_runs = 0 mcbeil_runs = 0 mcbece_runs = 0 - head = { - "Accept": "application/json", - "User-Agent":"mcbeDiscordBot/1.0" - } + head = {"Accept": "application/json", "User-Agent": "mcbeDiscordBot/1.0"} gameID = 'yd4ovvg1' # ID of Minecraft bedrock gameID2 = 'v1po7r76' # ID of Category extension runsRequest = requests.get( - f'https://www.speedrun.com/api/v1/runs?game={gameID}&status=new&max=200&embed=category,players,level&orderby=submitted', headers=head) + f'https://www.speedrun.com/api/v1/runs?game={gameID}&status=new&max=200&embed=category,players,level&orderby=submitted', + headers=head) runs = json.loads(runsRequest.text) runsRequest2 = requests.get( - f'https://www.speedrun.com/api/v1/runs?game={gameID2}&status=new&max=200&embed=category,players,level&orderby=submitted', headers=head) + f'https://www.speedrun.com/api/v1/runs?game={gameID2}&status=new&max=200&embed=category,players,level&orderby=submitted', + headers=head) runs2 = json.loads(runsRequest2.text) # Use https://www.speedrun.com/api/v1/games?abbreviation=mcbe for ID for game in range(2): for i in range(200): - leaderboard = '' # A little ugly but prevents name not defined error + leaderboard = '' # A little ugly but prevents name not defined error level = False try: for key, value in runs['data'][i].items(): + if key == 'id': + run_id = value if key == 'weblink': link = value if key == 'level': @@ -95,6 +101,11 @@ async def pendingRuns(self, ctx): categoryName = value["data"]["name"] if key == 'category' and not level: categoryName = value["data"]["name"] + if key == 'videos': + if value['links'][0]['uri'] in self.bot.video_blacklist: + await rejectRun( + self, self.bot.config['api_key'], ctx, run_id, + 'Detected as spam by our automatic filter') if key == 'players': if value["data"][0]['rel'] == 'guest': player = value["data"][0]['name'] @@ -113,30 +124,44 @@ async def pendingRuns(self, ctx): leaderboard = 'Individual Level Run' else: mcbe_runs += 1 - leaderboard = "Full Game Run" # If this doesn't work I'm starting a genocide + leaderboard = "Full Game Run" # If this doesn't work I'm starting a genocide elif game == 1: leaderboard = "Category Extension Run" mcbece_runs += 1 embed = discord.Embed( - title=leaderboard, url=link, description=f"{categoryName} in `{str(rta).replace('000','')}` by **{player}**", color=16711680+i*60, timestamp=timestamp) - await self.bot.get_channel(int(self.bot.config[str(ctx.message.guild.id)]["pending_channel"])).send(embed=embed) + title=leaderboard, + url=link, + description= + f"{categoryName} in `{str(rta).replace('000','')}` by **{player}**", + color=16711680 + i * 60, + timestamp=timestamp) + await self.bot.get_channel( + int(self.bot.config[str( + ctx.message.guild.id)]["pending_channel"]) + ).send(embed=embed) runs = runs2 gameID = gameID2 - embed_stats = discord.Embed(title='Pending Run Stats', description=f"Full Game Runs: {mcbe_runs}\nIndividual Level Runs: {mcbeil_runs}\nCategory Extension Runs: {mcbece_runs}", color=16711680 + i * 60) - await self.bot.get_channel(int(self.bot.config[str(ctx.message.guild.id)]["pending_channel"])).send(embed=embed_stats) + embed_stats = discord.Embed( + title='Pending Run Stats', + description= + f"Full Game Runs: {mcbe_runs}\nIndividual Level Runs: {mcbeil_runs}\nCategory Extension Runs: {mcbece_runs}", + color=16711680 + i * 60) + await self.bot.get_channel( + int(self.bot.config[str(ctx.message.guild.id)]["pending_channel"]) + ).send(embed=embed_stats) async def verifyNew(self, apiKey=None, userID=None): - if apiKey==None: + if apiKey == None: head = { - "Accept": "application/json", - "User-Agent": "mcbeDiscordBot/1.0" + "Accept": "application/json", + "User-Agent": "mcbeDiscordBot/1.0" } else: head = { - "X-API-Key": apiKey, - "Accept": "application/json", - "User-Agent": "mcbeDiscordBot/1.0" + "X-API-Key": apiKey, + "Accept": "application/json", + "User-Agent": "mcbeDiscordBot/1.0" } server = self.bot.get_guild(574267523869179904) # Troll is mentally challenged I guess ¯\_(ツ)_/¯ @@ -149,11 +174,13 @@ async def verifyNew(self, apiKey=None, userID=None): data = json.loads(Path('./api_keys.json').read_text()) if str(user.id) in data: - pbs = requests.get(f"https://www.speedrun.com/api/v1/users/{data[str(user.id)]}/personal-bests", headers=head) + pbs = requests.get( + f"https://www.speedrun.com/api/v1/users/{data[str(user.id)]}/personal-bests", + headers=head) pbs = json.loads(pbs.text) else: - r = requests.get( - 'https://www.speedrun.com/api/v1/profile', headers=head) + r = requests.get('https://www.speedrun.com/api/v1/profile', + headers=head) # print(r.text) if r.status_code >= 400: await user.send(f"```json\n{r.text}```") @@ -190,8 +217,8 @@ async def verifyNew(self, apiKey=None, userID=None): else: await server.get_member(user.id).remove_roles(RunneRole) -class Src(commands.Cog): +class Src(commands.Cog): def __init__(self, bot): self.bot = bot self.checker.start() @@ -203,7 +230,10 @@ class Src(commands.Cog): @commands.guild_only() async def pending(self, ctx): async with ctx.typing(): - await self.bot.get_channel(int(self.bot.config[str(ctx.message.guild.id)]["pending_channel"])).purge(limit=500) + await self.bot.get_channel( + int(self.bot.config[str( + ctx.message.guild.id)]["pending_channel"])).purge(limit=500 + ) await pendingRuns(self, ctx) @commands.command(description="Reject runs quickly") @@ -230,7 +260,9 @@ class Src(commands.Cog): if apiKey == None: data = json.loads(Path('./api_keys.json').read_text()) if not str(ctx.author.id) in data: - await ctx.send(f"Please try again this command by getting an apiKey from https://www.speedrun.com/api/auth then do `{ctx.prefix}verify <apiKey>` in my DMs or anywhere in this server. \nBe careful who you share this key with. To learn more check out https://github.com/speedruncomorg/api/blob/master/authentication.md") + await ctx.send( + f"Please try again this command by getting an apiKey from https://www.speedrun.com/api/auth then do `{ctx.prefix}verify <apiKey>` in my DMs or anywhere in this server. \nBe careful who you share this key with. To learn more check out https://github.com/speedruncomorg/api/blob/master/authentication.md" + ) return if ctx.guild != None: await ctx.message.delete() @@ -242,7 +274,7 @@ class Src(commands.Cog): @tasks.loop(minutes=10.0) async def checker(self): data = json.loads(Path('./api_keys.json').read_text()) - for key,value in data.items(): + for key, value in data.items(): await verifyNew(self, None, key) diff --git a/cogs/utils.py b/cogs/utils.py index bacf5e6..ab9bc42 100755 --- a/cogs/utils.py +++ b/cogs/utils.py @@ -118,7 +118,7 @@ class Utils(commands.Cog): return else: await ctx.send(error) - #await ctx.send(f"{ctx.message.author.display_name}, you have to wait {round(error.retry_after, 7)} seconds before using this again.") + #await ctx.send(f"{discord.utils.escape_mentions(ctx.message.author.display_name)}, you have to wait {round(error.retry_after, 7)} seconds before using this again.") @commands.command() async def findsleep(self, ctx): @@ -147,15 +147,15 @@ class Utils(commands.Cog): # Add extra comment based on number of sleepHrs if sleepHrs == 0: - await ctx.send(f"{ctx.message.author.display_name} -> your sleep is 0 hours long - nice try \:D") + await ctx.send(f"{discord.utils.escape_mentions(ctx.message.author.display_name)} -> your sleep is 0 hours long - nice try \:D") elif sleepHrs <= 5: if sleepHrs == 1: s = '' else: s = 's' - await ctx.send(f"{ctx.message.author.display_name} -> your sleep is {sleepHrs} hour{s} long - {lessSleepMsg[randint(0, len(lessSleepMsg) - 1)]}") + await ctx.send(f"{discord.utils.escape_mentions(ctx.message.author.display_name)} -> your sleep is {sleepHrs} hour{s} long - {lessSleepMsg[randint(0, len(lessSleepMsg) - 1)]}") else: - await ctx.send(f"{ctx.message.author.display_name} -> your sleep is {sleepHrs} hours long - {moreSleepMsg[randint(0, len(moreSleepMsg) - 1)]}") + await ctx.send(f"{discord.utils.escape_mentions(ctx.message.author.display_name)} -> your sleep is {sleepHrs} hours long - {moreSleepMsg[randint(0, len(moreSleepMsg) - 1)]}") @commands.Cog.listener() async def on_member_join(self, member): @@ -234,7 +234,7 @@ class Utils(commands.Cog): async def leaderboard_handler(self,ctx,error): if isinstance(error, commands.CommandOnCooldown): #return - await ctx.send(f"{ctx.message.author.display_name}, you have to wait {round(error.retry_after, 2)} seconds before using this again.") + await ctx.send(f"{discord.utils.escape_mentions(ctx.message.author.display_name)}, you have to wait {round(error.retry_after, 2)} seconds before using this again.") @commands.cooldown(1, 60, commands.BucketType.guild) @commands.command() @@ -1,37 +1,67 @@ import asyncio import logging +import json from colorama import init as init_colorama from bot import BedrockBot +def check_jsons(): + try: + f = open('config.json', 'r') + except FileNotFoundError: + token = input('BOT SETUP - Enter bot token: ') + with open('config.json', 'w+') as f: + json.dump({"token": token}, f, indent=4) + + try: + f = open('blacklist.json', 'r') + except FileNotFoundError: + with open('blacklist.json', 'w+') as f: + json.dump([], f, indent=4) + + try: + f = open('video_blacklist.json', 'r') + except FileNotFoundError: + with open('video_blacklist.json', 'w+') as f: + json.dump([], f, indent=4) + + def setup_logging(): - FORMAT = '%(asctime)s - [%(levelname)s]: %(message)s' - DATE_FORMAT = '%d/%m/%Y (%H:%M:%S)' + FORMAT = '%(asctime)s - [%(levelname)s]: %(message)s' + DATE_FORMAT = '%d/%m/%Y (%H:%M:%S)' - logger = logging.getLogger('discord') - logger.setLevel(logging.INFO) + logger = logging.getLogger('discord') + logger.setLevel(logging.INFO) - file_handler = logging.FileHandler(filename='discord.log', mode='a', encoding='utf-8') - file_handler.setFormatter(logging.Formatter(fmt=FORMAT, datefmt=DATE_FORMAT)) - file_handler.setLevel(logging.INFO) - logger.addHandler(file_handler) + file_handler = logging.FileHandler(filename='discord.log', + mode='a', + encoding='utf-8') + file_handler.setFormatter( + logging.Formatter(fmt=FORMAT, datefmt=DATE_FORMAT)) + file_handler.setLevel(logging.INFO) + logger.addHandler(file_handler) + + console_handler = logging.StreamHandler() + console_handler.setFormatter( + logging.Formatter(fmt=FORMAT, datefmt=DATE_FORMAT)) + console_handler.setLevel(logging.WARNING) + logger.addHandler(console_handler) - console_handler = logging.StreamHandler() - console_handler.setFormatter(logging.Formatter(fmt=FORMAT, datefmt=DATE_FORMAT)) - console_handler.setLevel(logging.WARNING) - logger.addHandler(console_handler) def run_bot(): - bot = BedrockBot() - bot.run() + bot = BedrockBot() + bot.run() + if __name__ == "__main__": - init_colorama(autoreset=True) + init_colorama(autoreset=True) + + setup_logging() - setup_logging() + check_jsons() - run_bot() + run_bot() @@ -1,13 +1,6 @@ # Minecraft Bedrock Discord Bot ## How to -Make a file called `config.json` and add -```json -{ - "token": "your_bot_token" -} -``` - Launch the bot with `python3 main.py` and you're ready to go. Unless dependencies. Dependencies are google cloud and discord. Install the dependencies with `python -m pip install -r requirements.txt` @@ -16,13 +9,17 @@ A few "dangerous" commands such as `!purge` are restriced to `bot_masters`, to a ```json { "token": "your_bot_token", - "bot_masters": <users_discord_id> + "<guild_id>": { + "bot_masters": <users_discord_id> + } } ``` +`guild_id` is the ID of the discord server in the form of a string while user IDs are integers + You can also use lists, for example: `"bot_masters": [280428276810383370, 99457716614885376]` -A user added as a botmaster will be able to edit the config via discord with the command `!setvar <var_name> <var_value>` +A user added as a botmaster will be able to edit the config via discord with the command `!setvar <var_name> <var_value>` `!setvar` also supports lists which can be added like so: `!setvar <var_name> [<index 0>, <index 1>]` -This bot was built as a fork of [celesteBot](https://github.com/CelesteClassic/celestebot), so a lot of code is recycled. +This bot was built as a fork of [celesteBot](https://github.com/CelesteClassic/celestebot), so a lot of code is recycled. Feel free to make a pull request or use the code here. |