import asyncio import aiohttp import time import sys import subprocess import os import json import traceback start_time = time.time() # Initialize the logger first so the colors and shit are setup from utils.logger import log log.init() from utils.bootstrap import Bootstrap Bootstrap.run_checks() from utils import checks from utils.language import Language from discord.ext import commands from utils.config import Config from utils.tools import * from utils.channel_logger import Channel_Logger from utils.mysql import * from utils.buildinfo import * config = Config() log.setupRotator(config.log_date_format, config.log_time_format) if config.debug: log.enableDebugging() # pls no flame bot = commands.AutoShardedBot(command_prefix=config.command_prefix, description="A multi-purpose Ruby Rose from RWBY themed discord bot", pm_help=None) channel_logger = Channel_Logger(bot) aiosession = aiohttp.ClientSession(loop=bot.loop) lock_status = config.lock_status extensions = [ "commands.fun", "commands.information", "commands.moderation", "commands.configuration", "commands.nsfw", "commands.music", "commands.reactions" ] # Thy changelog change_log = [ "Commands:", "+ nou", "+ cow", "+ fortune", "+ cows", "+ neko", "+ twitch", "+ youtube", "+ owo", "Other things:", "The avatar command will now display png instead of webp for non-animated avatars", "Fixed NSFW commands so they don't return the JSONDecodeError (hopefully)", "The defaultavatar command works again.", "The shardid command actually shows what shard you're on now" ] async def _restart_bot(): try: await aiosession.close() await bot.cogs["Music"].disconnect_all_voice_clients() except: pass await bot.logout() subprocess.call([sys.executable, "bot.py"]) async def _shutdown_bot(): try: aiosession.close() await bot.cogs["Music"].disconnect_all_voice_clients() except: pass await bot.logout() async def set_default_status(): if not config.enable_default_status: return type = config.default_status_type name = config.default_status_name try: type = discord.Status(type) except: type = discord.Status.online if name is not None: if config.default_status_type == "stream": if config.default_status_name is None: log.critical("If the status type is set to \"stream\" then the default status game must be specified") os._exit(1) status = discord.Activity(name=name, url="http://twitch.tv/ZeroEpoch1969", type=discord.ActivityType.streaming) else: status = discord.Activity(name=name, type=discord.ActivityType.playing) await bot.change_presence(status=type, activity=status) else: await bot.change_presence(status=type) @bot.event async def on_resumed(): log.info("Reconnected to discord!") @bot.event async def on_ready(): print("Connected!\n") print("Logged in as:\n{}/{}#{}\n----------".format(bot.user.id, bot.user.name, bot.user.discriminator)) print("Bot version: {}\nAuthor(s): {}\nCode name: {}\nBuild date: {}".format(BUILD_VERSION, BUILD_AUTHORS, BUILD_CODENAME, BUILD_DATE)) log.debug("Debugging enabled!") if config.enable_default_status: await set_default_status() else: await bot.change_presence(activity=discord.Activity(name="Zwei", type=discord.ActivityType.watching), status=discord.Status.dnd) for extension in extensions: try: bot.load_extension(extension) except Exception as e: log.error("Failed to load extension {}\n{}: {}".format(extension, type(e).__name__, e)) if os.path.isdir("data/music"): try: bot.cogs["Music"].clear_data() log.info("The music cache has been cleared!") except: log.warning("Failed to clear the music cache!") if config.enableMal: try: bot.load_extension("commands.myanimelist") log.info("The MyAnimeList module has been enabled!") except Exception as e: log.error("Failed to load the MyAnimeList module\n{}: {}".format(type(e).__name__, e)) if config.enableOsu: log.info("The osu! module has been enabled in the config!") if config._dbots_token: log.info("Updating DBots Statistics...") try: r = requests.post("https://bots.discord.pw/api/bots/{}/stats".format(bot.user.id), json={ "server_count": len(bot.guilds)}, headers={"Authorization": config._dbots_token}, timeout=5) if r.status_code == "200": log.info("Discord Bots guild count updated.") elif r.status_code == "401": log.error("An error occurred while trying to update the guild count!") except requests.exceptions.Timeout: log.error("Failed to update the guild count: request timed out.") if config._carbonitex_key: log.info("Updating Carbonitex Statistics...") payload = {"key": config._carbonitex_key, "guildcount": len(bot.guilds), "botname": bot.user.name, "logoid": bot.user.avatar_url} owner = discord.utils.get(list(bot.get_all_members()), id=config.owner_id) if owner is not None: payload["ownername"] = owner.name try: r = requests.post("https://www.carbonitex.net/discord/data/botdata.php", json=payload, timeout=5) if r.text == "1 - Success": log.info("Carbonitex stats updated") else: log.error("Failed to update the carbonitex stats, double check the key in the config!") except requests.exceptions.Timeout: log.error("Failed to update the carbonitex stats: request timed out") if config.enableSteam: if not config._steamAPIKey: log.warning("The steam module was enabled but no steam web api key was specified, disabling...") else: bot.load_extension("commands.steam") log.info("The steam module has been enabled!") @bot.event async def on_command_error(ctx, error): if isinstance(error, commands.CommandNotFound): return if isinstance(error, commands.DisabledCommand): await ctx.send(Language.get("bot.errors.disabled_command", ctx)) return if isinstance(error, checks.owner_only): await ctx.send(Language.get("bot.errors.owner_only", ctx)) return if isinstance(error, checks.dev_only): await ctx.send(Language.get("bot.errors.dev_only", ctx)) return if isinstance(error, checks.support_only): await ctx.send(Language.get("bot.errors.support_only", ctx)) return if isinstance(error, checks.not_nsfw_channel): await ctx.send(Language.get("bot.errors.not_nsfw_channel", ctx)) return if isinstance(error, checks.not_guild_owner): await ctx.send(Language.get("bot.errors.not_guild_owner", ctx)) return if isinstance(error, checks.no_permission): await ctx.send(Language.get("bot.errors.no_permission", ctx)) return if isinstance(error, commands.NoPrivateMessage): await ctx.send(Language.get("bot.errors.no_private_message", ctx)) return if isinstance(ctx.channel, discord.DMChannel): await ctx.send(Language.get("bot.errors.command_error_dm_channel", ctx)) return #In case the bot failed to send a message to the channel, the try except pass statement is to prevent another error try: await ctx.send(Language.get("bot.errors.command_error", ctx).format(error)) except: pass log.error("An error occured while executing the {} command: {}".format(ctx.command.qualified_name, error)) @bot.before_invoke async def on_command_preprocess(ctx): if isinstance(ctx.channel, discord.DMChannel): guild = "Private Message" else: guild = "{}/{}".format(ctx.guild.id, ctx.guild.name) log.info("[Command] [{}] [{}/{}]: {}".format(guild, ctx.author.id, ctx.author, ctx.message.content)) @bot.event async def on_message(message): if message.author.bot: return if isinstance(message.author, discord.Member): if discord.utils.get(message.author.roles, name="Grimm"): return if getblacklistentry(message.author.id) is not None: return await bot.process_commands(message) @bot.event async def on_member_join(member:discord.Member): join_message = read_data_entry(member.guild.id, "join-message") if join_message is not None: join_message = join_message.replace("%user%", member.mention).replace("%server%", member.guild.name) join_leave_channel_id = read_data_entry(member.guild.id, "join-leave-channel") if join_leave_channel_id is not None: join_leave_channel = discord.utils.get(member.guild.channels, id=join_leave_channel_id) if join_leave_channel is None: update_data_entry(member.guild.id, "join-leave-channel", None) else: join_leave_channel = None join_role_id = read_data_entry(member.guild.id, "join-role") if join_role_id is not None: join_role = discord.utils.get(member.guild.roles, id=join_role_id) if join_role is None: update_data_entry(member.guild.id, "join-role", None) else: join_role = None if join_leave_channel is not None and join_message is not None: try: await join_leave_channel.send(join_message) except: pass if join_role is not None: try: await member.add_roles(join_role) except: None @bot.event async def on_member_remove(member:discord.Member): leave_message = read_data_entry(member.guild.id, "leave-message") if leave_message is not None: leave_message = leave_message.replace("%user%", member.mention).replace("%guild%", member.guild.name) join_leave_channel_id = read_data_entry(member.guild.id, "join-leave-channel") if join_leave_channel_id is not None: join_leave_channel = discord.utils.get(member.guild.channels, id=join_leave_channel_id) if join_leave_channel is None: update_data_entry(member.guild.id, "join-leave-channel", None) else: join_leave_channel = None if join_leave_channel is not None and leave_message is not None: try: await join_leave_channel.send(leave_message) except: pass @bot.command(hidden=True) @checks.is_dev() async def debug(ctx, *, shit:str): """This is the part where I make 20,000 typos before I get it right""" # "what the fuck is with your variable naming" - EJH2 # seth seriously what the fuck - Robin import os import random import re from datetime import datetime, timedelta try: rebug = eval(shit) if asyncio.iscoroutine(rebug): rebug = await rebug await ctx.send(py.format(rebug)) except Exception as damnit: await ctx.send(py.format("{}: {}".format(type(damnit).__name__, damnit))) @bot.command(hidden=True) @checks.is_owner() async def rename(ctx, *, name:str): """Renames the bot""" await bot.user.edit(username=name) await ctx.send("si") @bot.command(hidden=True) @checks.is_dev() async def shutdown(ctx): """Shuts down the bot""" await ctx.send("Shutting down...") log.warning("{} has shut down the bot!".format(ctx.author)) await _shutdown_bot() @bot.command(hidden=True) @checks.is_dev() async def restart(ctx): """Restarts the bot""" await ctx.send("Restarting...") log.warning("{} has restarted the bot!".format(ctx.author)) await _restart_bot() @bot.command(hidden=True) @checks.is_owner() async def setavatar(ctx, *, url:str=None): """Changes the bot's avatar""" if ctx.message.attachments: url = ctx.message.attachments[0].url elif url is None: await ctx.send("Please specify an avatar url if you did not attach a file") return try: with aiohttp.Timeout(10): async with aiosession.get(url.strip("<>")) as image: await bot.user.edit(avatar=await image.read()) except Exception as e: await ctx.send("Unable to change avatar: {}".format(e)) return await ctx.send(":eyes:") @bot.command() async def notifydev(ctx, *, message:str): """Sends a message to the developers""" if isinstance(ctx.channel, discord.DMChannel): guild = "`No server! Sent via Private Message!`" else: guild = "`{}` / `{}`".format(ctx.guild.id, ctx.guild.name) msg = make_message_embed(ctx.author, 0xFF0000, message, formatUser=True) owner = bot.get_user(config.owner_id) if owner: await owner.send("You have received a new message! The user's ID is `{}` Server: {}".format(ctx.author.id, guild), embed=msg) for id in config.dev_ids: dev = bot.get_user(id) if dev: await dev.send("You have received a new message! The user's ID is `{}` Server: {}".format(ctx.author.id, guild), embed=msg) for id in config.support_ids: support_member = bot.get_user(id) if support_member: await support_member.send("You have received a new message! The user's ID is `{}` Server: {}".format(ctx.author.id, guild), embed=msg) await ctx.author.send(Language.get("bot.dev_notify", ctx).format(message)) @bot.command() async def suggest(ctx, *, suggestion:str): """Sends a suggestion to the developers""" if isinstance(ctx.channel, discord.DMChannel): guild = "`No server! Sent via Private Message!`" else: guild = "`{}` / `{}`".format(ctx.guild.id, ctx.guild.name) msg = make_message_embed(ctx.author, 0xFF0000, suggestion, formatUser=True) owner = bot.get_user(config.owner_id) if owner: await owner.send("You have received a new suggestion! The user's ID is `{}` Server: {}".format(ctx.author.id, guild), embed=msg) for id in config.dev_ids: dev = bot.get_user(id) if dev: await dev.send("You have received a new suggestion! The user's ID is `{}` Server: {}".format(ctx.author.id, guild), embed=msg) for id in config.support_ids: support_member = bot.get_user(id) if support_member: await support_member.send("You have received a new message! The user's ID is `{}` Server: {}".format(ctx.author.id, guild), embed=msg) await ctx.author.send(Language.get("bot.errors.dev_suggest", ctx).format(suggestion)) @bot.command(hidden=True) @checks.is_dev() async def blacklist(ctx, id:int, *, reason:str): """Blacklists a user""" await ctx.channel.trigger_typing() user = discord.utils.get(list(bot.get_all_members()), id=id) if user is None: await ctx.send("Could not find a user with an id of `{}`".format(id)) return if getblacklistentry(id) != None: await ctx.send("`{}` is already blacklisted".format(user)) return blacklistuser(id, user.name, user.discriminator, reason) await ctx.send("Blacklisted `{}` Reason: `{}`".format(user, reason)) try: await user.send("You have been blacklisted from the bot by `{}` Reason: `{}`".format(ctx.author, reason)) except: log.debug("Couldn't send a message to a user with an ID of \"{}\"".format(id)) await channel_logger.log_to_channel(":warning: `{}` blacklisted `{}`/`{}` Reason: `{}`".format(ctx.author, id, user, reason)) @bot.command(hidden=True) @checks.is_dev() async def unblacklist(ctx, id:int): """Unblacklists a user""" entry = getblacklistentry(id) if entry is None: await ctx.send("No blacklisted user can be found with an id of `{}`".format(id)) return try: unblacklistuser(id) except: await ctx.send("No blacklisted user can be found with an id of `{}`".format(id)) # Don't ask pls return await ctx.send("Successfully unblacklisted `{}#{}`".format(entry.get("name"), entry.get("discrim"))) try: await discord.User(id=id).send("You have been unblacklisted from the bot by `{}`".format(ctx.author)) except: log.debug("Couldn't send a message to a user with an ID of \"{}\"".format(id)) await channel_logger.log_to_channel(":warning: `{}` unblacklisted `{}`/`{}#{}`".format(ctx.author, id, entry.get("name"), entry.get("discrim"))) @bot.command() @checks.is_dev() async def showblacklist(ctx): """Shows the list of users that are blacklisted from the bot""" blacklist = getblacklist() count = len(blacklist) if blacklist == []: blacklist = "There are no blacklisted users" else: blacklist = "\n".join(blacklist) await ctx.send(xl.format("Total blacklisted users: {}\n\n{}".format(count, blacklist))) @bot.command(hidden=True) @checks.is_owner() async def lockstatus(ctx): """Toggles the lock on the status""" global lock_status if lock_status: lock_status = False await ctx.send("The status has been unlocked") else: lock_status = True await ctx.send("The status has been locked") @bot.command() async def stream(ctx, *, name:str): """Sets the streaming status with the specified name""" if lock_status: if not ctx.author.id == config.owner_id and not ctx.author.id in config.dev_ids: await ctx.send(Language.get("bot.status_locked", ctx)) return await bot.change_presence(activity=discord.Activity(name=name, type=discord.ActivityType.streaming, url="https://www.twitch.tv/ZeroEpoch1969")) await ctx.send(Language.get("bot.now_streaming", ctx).format(name)) await channel_logger.log_to_channel(":information_source: `{}`/`{}` has changed the streaming status to `{}`".format(ctx.author.id, ctx.author, name)) @bot.command() async def changestatus(ctx, status:str, *, name:str=None): """Changes the bot's status with the specified status type and name""" if lock_status: if not ctx.author.id == config.owner_id and not ctx.author.id in config.dev_ids: await ctx.send(Language.get("bot.status_locked", ctx)) return activity = None if status == "invisible" or status == "offline": await ctx.send(Language.get("bot.forbidden_status_type", ctx).format(status)) return try: statustype = discord.Status(status) except ValueError: await ctx.send(Language.get("bot.valid_status_types", ctx)) return if name != "": activity = discord.Activity(name=name, type=discord.ActivityType.playing) await bot.change_presence(activity=activity, status=statustype) if name is not None: await ctx.send(Language.get("bot.status_change_with_name", ctx).format(name, status)) await channel_logger.log_to_channel(":information_source: `{}`/`{}` has changed the game name to `{}` with a(n) `{}` status type".format(ctx.author.id, ctx.author, name, status)) else: await ctx.send(Language.get("bot.status_change", ctx).format(status)) await channel_logger.log_to_channel(":information_source: `{}`/`{}` has changed the status type to `{}`".format(ctx.author.id, ctx.author, name)) @bot.command(hidden=True) @checks.is_dev() async def terminal(ctx, *, command:str): """Runs terminal commands and shows the output via a message. Oooh spoopy!""" try: await ctx.channel.trigger_typing() await ctx.send(xl.format(subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[0].decode("ascii"))) except: await ctx.send("Error, couldn't send command") @bot.command(hidden=True) @checks.is_dev() async def uploadfile(ctx, *, path:str): """Uploads any file on the system. What is this hackery?""" await ctx.channel.trigger_typing() try: await ctx.send(file=discord.File(path)) except FileNotFoundError: await ctx.send("That file does not exist!") @bot.command() async def changelog(ctx): """The latest changelog""" await ctx.send(Language.get("bot.changelog", ctx).format(bot.command_prefix, diff.format("\n".join(map(str, change_log))))) @bot.command() async def version(ctx): """Get the bot's current version""" await ctx.send(Language.get("bot.version", ctx).format(BUILD_VERSION, BUILD_AUTHORS, BUILD_CODENAME, BUILD_DATE)) @bot.command(hidden=True) @checks.is_support() async def dm(ctx, id:int, *, message:str): """DMs a user""" msg = make_message_embed(ctx.author, 0xFF0000, message, formatUser=True) user = bot.get_user(id) if not user: await ctx.send("Could not find any user with an ID of `{}`".format(id)) owner = bot.get_user(config.owner_id) if owner: await owner.send("`{}` has replied to a recent DM with `{}` (ID: `{}`)".format(ctx.author, user, id), embed=msg) for id in config.dev_ids: dev = bot.get_user(id) if dev: await dev.send("`{}` has replied to a recent DM with `{}` (ID: `{}`)".format(ctx.author, user, id), embed=msg) await user.send("You have received a message from one of the bot developers!", embed=msg) @bot.command() async def uptime(ctx): """Displays how long the bot has been online for""" second = time.time() - start_time minute, second = divmod(second, 60) hour, minute = divmod(minute, 60) day, hour = divmod(hour, 24) week, day = divmod(day, 7) await ctx.send(Language.get("bot.uptime", ctx) % (week, day, hour, minute, second)) @bot.command(hidden=True) @checks.is_dev() async def reload(ctx, *, extension:str): """Reloads an extension""" extension = "commands.{}".format(extension) if extension in extension: await ctx.send("Reloading {}...".format(extension)) bot.unload_extension(extension) try: bot.load_extension(extension) except: await ctx.send(py.format(traceback.format_exc())) return await ctx.send("Successfully reloaded the {} extension!".format(extension)) else: await ctx.send("Extension doesn't exist") @bot.command() async def joinserver(ctx): """Sends the bot's OAuth2 link""" await ctx.author.send(Language.get("bot.joinserver", ctx).format("http://invite.ruby.zeroepoch1969.rip")) @bot.command() async def invite(ctx): """Sends an invite link to the bot's server""" await ctx.author.send(Language.get("bot.invite", ctx).format("https://discord.gg/RJTFyBd", bot.command_prefix)) @bot.command() async def ping(ctx): """Pings the bot""" pingms = await ctx.send(Language.get("bot.pinging", ctx)) start = time.time() async with aiosession.get("https://discordapp.com"): duration = time.time() - start duration = round(duration * 1000) await pingms.edit(content="{0} // **{1}ms**".format(pingms.content, duration)) @bot.command() async def website(ctx): """Gives the link to the bot docs""" await ctx.send(Language.get("bot.website", ctx)) @bot.command() async def github(ctx): """Gives the link to the github repo""" await ctx.send(Language.get("bot.github", ctx)) @bot.command() async def stats(ctx): """Gets the bot's stats""" voice_clients = [] for guild in bot.guilds: if guild.me.voice: voice_clients.append(guild.me.voice.channel) fields = {Language.get("bot.stats.users", ctx):len(list(bot.get_all_members())), Language.get("bot.stats.servers", ctx):len(bot.guilds), Language.get("bot.stats.channels", ctx):len(list( bot.get_all_channels())), Language.get("bot.stats.voice_clients", ctx):len(voice_clients), Language.get("bot.stats.discordpy_version", ctx):discord.__version__, Language.get("bot.stats.bot_version", ctx): BUILD_VERSION, Language.get("bot.stats.built_by", ctx):BUILD_AUTHORS, Language.get("bot.stats.translators", ctx):", ".join(TRANSLATORS.keys())} embed = make_list_embed(fields) embed.title = str(bot.user) embed.color = 0xFF0000 embed.set_thumbnail(url=bot.user.avatar_url) bot_owner = discord.utils.get(list(bot.get_all_members()), id=config.owner_id) if bot_owner is not None: embed.set_footer(text=bot_owner, icon_url=get_avatar(bot_owner)) await ctx.send(embed=embed) @bot.command() @commands.guild_only() @checks.is_dev() async def editmessage(ctx, id:int, *, newmsg:str): """Edits a message sent by the bot""" try: msg = await ctx.channel.get_message(id) except discord.errors.NotFound: await ctx.send("Couldn't find a message with an ID of `{}` in this channel".format(id)) return if msg.author != ctx.guild.me: await ctx.send("That message was not sent by me") return await msg.edit(content=newmsg) await ctx.send("edit af") @bot.command() async def top10servers(ctx): """Gets the top 10 most populated servers the bot is on""" guilds = [] for guild in sorted(bot.guilds, key=lambda e: e.member_count, reverse=True)[:10]: members = 0 bots = 0 total = len(guild.members) for member in guild.members: if member.bot: bots += 1 else: members += 1 guilds.append(Language.get("bot.top10servers", ctx).format(guild.name, members, bots, total)) await ctx.send("```{}```".format("\n\n".join(guilds))) @bot.command(hidden=True, enable=False) async def vote(ctx, vote:str): """Vote command""" # Left this code here for future purposes so in the event I run another poll I don't need to recode a vote command with open("data/votes.json", "r") as jsonFile: votes = json.load(jsonFile) voted = False try: votes[ctx.author.id] voted = True except KeyError: pass if voted: await ctx.send("You already voted!") return vote = vote.lower() if vote == "yes" or vote == "no": votes[ctx.author.id] = vote await ctx.send("Successfully voted!") else: await ctx.send("Valid vote options are `yes` and `no`") return with open("data/ranksysvotes.json", "w") as jsonFile: json.dump(votes, jsonFile) @bot.command(hidden=True, enable=False) async def ranksysvoteresults(ctx): """Vote results command""" # Left this code here for future purposes so in the event I run another poll I don't need to recode a vote results command with open("data/votes.json", "r") as jsonFile: votes = json.load(jsonFile) yes = 0 no = 0 for vote in votes.values(): if vote == "yes": yes += 1 elif vote == "no": no += 1 await ctx.send("Results for the rank system poll:\nYes: {}\nNo: {}".format(yes, no)) @bot.command(hidden=True) async def test(ctx): await ctx.send("owo") @commands.guild_only() @checks.server_mod_or_perms(manage_server=True) @bot.command() async def setlanguage(ctx, language:str): """Sets the bot's language for the server""" await ctx.send(Language.set_language(ctx.guild, language)) @bot.command() async def translators(ctx): """Lists all of the bot's translators""" embed = make_list_embed(TRANSLATORS) embed.title = Language.get("bot.stats.translators", ctx) embed.color = 0xFF0000 await ctx.send(embed=embed) @commands.guild_only() @bot.command() async def shardid(ctx): """Display what shard you're on and count how many total shards exist""" await ctx.send("{}/{}".format(ctx.guild.shard_id, bot.shard_count)) print("Connecting...") bot.run(config._token)