def __init__(self, bot): super().__init__(bot) self.config = CogConfig("voice") self.text = CogText("voice") self.locked = []
def __init__(self, bot): super().__init__(bot) self.text = CogText("meme") self.config = CogConfig("meme") self.fishing_pool = self.config.get("_fishing")
def __init__(self, bot: commands.Bot): super().__init__(bot) self.config = CogConfig("base") self.text = CogText("base") self.status_loop.start() self.status = "online"
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("animals") self.text = CogText("animals") self.channel = None self.role = None
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("roles") self.text = CogText("roles") self.limit_programmes = {} self.limit_interests = {}
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("warden") self.text = CogText("warden") self.limit_full = 3 self.limit_hard = 7 self.limit_soft = 14
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("admin") self.text = CogText("admin") self.usage = {} self.jail_check.start() self.just_booted = True
def __init__(self, bot): super().__init__(bot) self.text = CogText("howto") try: self.data = hjson.load(open("data/howto/howto.hjson")) except: print("Howto init(): Could not load HOWTO source file." ) # noqa: T001 self.data = OrderedDict()
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("animals") self.text = CogText("animals") self.channel = None self.role = None # Because the API doesn't return the avatar resource immediately, # sometimes nothing happens, because the client caches the 404 # response (?). This is an attempt to counter that. self.check_delay = 10
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("actress") self.text = CogText("actress") self.supported_formats = ("jpg", "jpeg", "png", "webm", "mp4", "gif") self.path = os.getcwd() + "/data/actress/" try: self.reactions = hjson.load(open(self.path + "reactions.hjson")) except: self.reactions = {} self.usage = {}
def __init__(self, bot: commands.Bot): super().__init__(bot) self.config = CogConfig("sync") self.text = CogText("sync") self.slave_guild_id = self.config.get("slave_guild_id") self.engineer_ids = self.config.get("roles", "master_engineer_ids") self.slave_verify_id = self.config.get("roles", "slave_verify_id") self.mapping_ids = self.config.get("roles", "mapping") self.mapping = {} self.slave_guild = None self.slave_verify = None
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("points") self.text = CogText("points") self.limits_message = self.config.get("points_message") self.timer_message = self.config.get("timer_message") self.limits_reaction = self.config.get("points_reaction") self.timer_reaction = self.config.get("timer_reaction") self.stats_message = {} self.stats_reaction = {} self.cleanup.start()
class Account(rubbercog.Rubbercog): """Manage the bot account""" def __init__(self, bot): super().__init__(bot) self.text = CogText("account") @commands.is_owner() @commands.group(name="set") async def set(self, ctx): """Set attributes""" await utils.send_help(ctx) @set.command(name="name", aliases=["username"]) async def set_name(self, ctx, *, name: str): """Set bot name""" await self.bot.user.edit(username=name) await ctx.send(self.text.get("name", "ok", name=name)) @set.command(name="avatar", aliases=["image"]) async def set_avatar(self, ctx, *, path: str): """Set bot avatar path: path to image file, starting in `data/` directory """ if ".." in path: return await self.output.error(ctx, self.text.get("avatar", "invalid")) try: with open("data/" + path, "rb") as image: with ctx.typing(): await self.bot.user.edit(avatar=image.read()) except FileNotFoundError: return await self.output.error( ctx, self.text.get("avatar", "not_found")) await ctx.send(self.text.get("avatar", "ok"))
def __init__(self, bot): super().__init__(bot) self.text = CogText("verify") self.config = CogConfig("verify")
class Verify(rubbercog.Rubbercog): """Verify your account""" def __init__(self, bot): super().__init__(bot) self.text = CogText("verify") self.config = CogConfig("verify") ## ## Commands ## @commands.check(acl.check) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command() async def verify(self, ctx, email: str): """Ask for verification code""" await utils.delete(ctx) if email.count("@") != 1: raise NotAnEmail() if self.config.get("placeholder") in email: raise PlaceholderEmail() # check the database for member ID if repo_u.get(ctx.author.id) is not None: raise IDAlreadyInDatabase() # check the database for email if repo_u.getByLogin(email) is not None: raise EmailAlreadyInDatabase() # check e-mail format role = await self._email_to_role(ctx, email) # generate code code = await self._add_user(ctx.author, login=email, role=role) # send mail try: await self._send_verification_email(ctx.author, email, code) except smtplib.SMTPException as e: await self.console.warning(ctx, type(e).__name__) await self._send_verification_email(ctx.author, email, code) anonymised = "[redacted]@" + email.split("@")[1] await ctx.send( self.text.get( "verify successful", mention=ctx.author.mention, email=anonymised, prefix=config.prefix, ), delete_after=config.get("delay", "verify"), ) @commands.check(acl.check) @commands.cooldown(rate=3, per=120, type=commands.BucketType.user) @commands.command() async def submit(self, ctx, code: str): """Submit verification code""" await utils.delete(ctx) db_user = repo_u.get(ctx.author.id) if db_user is None or db_user.status in ( "unknown", "unverified") or db_user.code is None: raise SubmitWithoutCode() if db_user.status != "pending": raise ProblematicVerification(status=db_user.status, login=db_user.login) # repair the code code = code.replace("I", "1").replace("O", "0").upper() if code != db_user.code: raise WrongVerificationCode(ctx.author, code, db_user.code) # user is verified now repo_u.save_verified(ctx.author.id) # add role await self._add_verify_roles(ctx.author, db_user) # send messages for role_id in config.get("roles", "native"): if role_id in [x.id for x in ctx.author.roles]: await ctx.author.send(self.text.get("verification DM native")) break else: await ctx.author.send(self.text.get("verification DM guest")) # fmt: off # announce the verification await ctx.channel.send(self.text.get( "verification public", mention=ctx.author.mention, role=db_user.group, ), delete_after=config.get("delay", "verify")) # fmt: on await self.event.user( ctx, f"User {ctx.author.id} verified with group **{db_user.group}**.", ) ## ## Listeners ## @commands.Cog.listener() async def on_member_join(self, member: discord.Member): """Add them their roles back, if they have been verified before""" if member.guild.id != config.guild_id: return db_user = repo_u.get(member.id) if db_user is None or db_user.status != "verified": return # user has been verified, give them their main roles back await self._add_verify_roles(member, db_user) await self.event.user(member, f"Verification skipped (**{db_user.group}**)") ## ## Helper functions ## async def _email_to_role(self, ctx, email: str) -> discord.Role: """Get role from email address""" registered = self.config.get("suffixes") constraints = self.config.get("constraints") username = email.split("@")[0] for domain, role_id in list(registered.items())[:-1]: if not email.endswith(domain): continue # found corresponding domain, check constraint if domain in constraints: constraint = constraints[domain] else: constraint = list(constraints.values())[-1] match = re.fullmatch(constraint, username) # return if match is not None: return self.getGuild().get_role(role_id) else: await self.event.user( ctx, f"Rejecting e-mail: {self.sanitise(email)}") raise BadEmail(constraint=constraint) # domain not found, fallback to basic guest role role_id = registered.get(".") constraint = list(constraints.values())[-1] match = re.fullmatch(constraint, username) # return if match is not None: return self.getGuild().get_role(role_id) else: await self.event.user(ctx, f"Rejecting e-mail: {self.sanitise(email)}") raise BadEmail(constraint=constraint) async def _add_user(self, member: discord.Member, login: str, role: discord.Role) -> str: code_source = string.ascii_uppercase.replace("O", "").replace( "I", "") + string.digits code = "".join(random.choices(code_source, k=8)) repo_u.add(discord_id=member.id, login=login, group=role.name, code=code) await self.event.user( member, f"Adding {member.id} to database (**{role.name}**, code `{code}`)." ) return code async def _update_user(self, member: discord.Member) -> str: code_source = string.ascii_uppercase.replace("O", "").replace( "I", "") + string.digits code = "".join(random.choices(code_source, k=8)) repo_u.update(discord_id=member.id, code=code, status="pending") await self.event.user(member, f"{member.id} updated with code `{code}`") return code async def _send_verification_email(self, member: discord.Member, email: str, code: str) -> bool: cleartext = self.text.get("plaintext mail").format( guild_name=self.getGuild().name, code=code, bot_name=self.bot.user.name, git_hash=utils.git_get_hash()[:7], prefix=config.prefix, ) richtext = self.text.get( "html mail", # styling color_bg="#54355F", color_fg="white", font_family="Arial,Verdana,sans-serif", # names guild_name=self.getGuild().name, bot_name=self.bot.user.name, user_name=member.name, # codes code=code, git_hash=utils.git_get_hash()[:7], prefix=config.prefix, # images bot_avatar=self.bot.user.avatar_url_as(static_format="png", size=128), bot_avatar_size="120px", user_avatar=member.avatar_url_as(static_format="png", size=32), user_avatar_size="20px", ) msg = MIMEMultipart("alternative") msg["Subject"] = self.text.get("mail subject", guild_name=self.getGuild().name, user_name=member.name) msg["From"] = self.config.get("email", "address") msg["To"] = email msg["Bcc"] = self.config.get("email", "address") msg.attach(MIMEText(cleartext, "plain")) msg.attach(MIMEText(richtext, "html")) with smtplib.SMTP(self.config.get("email", "server"), self.config.get("email", "port")) as server: server.starttls() server.ehlo() server.login(self.config.get("email", "address"), self.config.get("email", "password")) server.send_message(msg) async def _add_verify_roles(self, member: discord.Member, db_user: object): """Return True if reverified""" verify = self.getVerifyRole() group = discord.utils.get(self.getGuild().roles, name=db_user.group) await member.add_roles(verify, group, reason="Verification") ## ## Error catching ## @commands.Cog.listener() async def on_command_error(self, ctx: commands.Context, error): # try to get original error if hasattr(ctx.command, "on_error") or hasattr(ctx.command, "on_command_error"): return error = getattr(error, "original", error) # non-rubbergoddess exceptions are handled globally if not isinstance(error, rubbercog.RubbercogException): return # fmt: off # exceptions with parameters if isinstance(error, ProblematicVerification): await self.output.warning( ctx, self.text.get("ProblematicVerification", status=error.status)) await self.event.user( ctx, f"Problem with verification: {error.login}: {error.status}") elif isinstance(error, BadEmail): await self.output.warning( ctx, self.text.get("BadEmail", constraint=error.constraint)) elif isinstance(error, WrongVerificationCode): await self.output.warning( ctx, self.text.get("WrongVerificationCode", mention=ctx.author.mention)) await self.event.user( ctx, f"User ({error.login}) code mismatch: `{error.their}` != `{error.database}`" ) # exceptions without parameters elif isinstance(error, VerificationException): await self.output.error(ctx, self.text.get(type(error).__name__))
class Shop(rubbercog.Rubbercog): """Make use of your karma""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("shop") self.text = CogText("shop") @commands.command() async def shop(self, ctx): """Display prices for various services""" embed = self.embed( ctx=ctx, title=self.text.get("info", "title"), description=self.text.get("info", "description"), ) embed.add_field( name=self.text.get("info", "set"), value=self.config.get("set"), ) embed.add_field( name=self.text.get("info", "reset"), value=self.config.get("reset"), ) await ctx.send(embed=embed) await utils.room_check(ctx) @commands.bot_has_permissions(manage_nicknames=True) @commands.check(acl.check) @commands.group(name="nickname") async def nickname(self, ctx): """Change your nickname""" await utils.send_help(ctx) @commands.cooldown(rate=5, per=60, type=commands.BucketType.member) @commands.check(acl.check) @nickname.command(name="set") async def nickname_set(self, ctx, *, nick: str): """Set the nickname. Use command `shop` to see prices Attributes ---------- nick: Your new nickname """ # stop if user does not have nickname set if ctx.author.nick is None and nick is None or not len(nick): return await ctx.send(self.text.get("no_nick", mention=ctx.author.mention)) # check if user has karma if self.get_user_karma(ctx.author.id) < self.config.get("set"): return await ctx.send( self.text.get( "not_enough_karma", mention=ctx.author.mention, ) ) for char in ("@", "#", "`", "'", '"'): if char in nick: return await ctx.send(self.text.get("bad_character", mention=ctx.author.mention)) # set nickname try: await ctx.author.edit(nick=nick, reason="Nickname purchase") except discord.Forbidden: return await ctx.send(self.text.get("higher_role")) repo_k.updateMemberKarma(ctx.author.id, -1 * self.price_nick) await ctx.send( self.text.get( "new_nick", mention=ctx.author.mention, nick=discord.utils.escape_markdown(nick), value=self.price_nick, ) ) await self.event.user(ctx, f"Nickname changed to {nick}.") @commands.cooldown(rate=1, per=60, type=commands.BucketType.member) @commands.check(acl.check) @nickname.command(name="unset") async def nickname_unset(self, ctx): """Unset the nickname""" if ctx.author.nick is None: return await ctx.send(self.text.get("no_nick", mention=ctx.author.mention)) # check if user has karma if self.get_user_karma(ctx.author.id) < self.config.get("reset"): return await ctx.send( self.text.get( "not_enough_karma", mention=ctx.author.mention, ) ) nick = ctx.author.nick await ctx.author.edit(nick=None, reason="Nickname reset") await ctx.send( self.text.get( "nick_removed", mention=ctx.author.mention, nick=self.sanitise(nick), ) ) await self.event.user(ctx, "Nickname reset.") ## ## Logic ## def get_user_karma(self, user_id: int) -> int: return getattr(repo_k.getMember(user_id), "karma", 0)
def __init__(self, bot): super().__init__(bot) self.text = CogText("mover")
class Mover(rubbercog.Rubbercog): """Move database objects""" def __init__(self, bot): super().__init__(bot) self.text = CogText("mover") ## ## Commands ## @commands.check(acl.check) @commands.group(name="move") async def move(self, ctx): """Move stuff""" await utils.send_help(ctx) @commands.check(acl.check) @move.command(name="member") async def move_member(self, ctx, before: discord.Member, after: discord.Member): """Move old member data to new one Roles from the `before` member are moved to the `after` one. """ async with ctx.typing(): result = self.move_user_data(before.id, after.id) try: await self.move_member_roles(before, after) except Exception as e: await self.console.error(ctx, "Could not migrate member roles", e) embed = self.move_user_embed(ctx, after, result) await ctx.send(embed=embed) @commands.check(acl.check) @move.command(name="user") async def move_user(self, ctx, before: int, after: int): """Move old user data to new one This is useful when the member is not on the server anymore. """ async with ctx.typing(): result = self.move_user_data(before, after) embed = self.move_user_embed(ctx, after, result) await ctx.send(embed=embed) ## ## Logic ## def move_user_data(self, before_id: int, after_id: int) -> Dict[str, int]: """Move user data Arguments --------- before_id: `int` Old user ID after_id: `int` New user ID Returns ------- `dict`: mapping from table name to change counter """ result = {} result["interaction"] = repo_interaction.move_user(before_id, after_id) result["karma"] = repo_karma.move_user(before_id, after_id) result["points"] = repo_points.move_user(before_id, after_id) result["user"] = repo_user.move_user(before_id, after_id) return result async def move_member_roles(self, before: discord.Member, after: discord.Member): """Move roles from the before member to the after one.""" roles = before.roles[1:] await after.add_roles(*roles, reason="Member migration") await before.remove_roles(*roles, reason="Member migration") ## ## Helper functions ## def move_user_embed( self, ctx, after: Union[discord.Member, int], result: Dict[str, int], ) -> discord.Embed: """Create embed for move_member and move_user Arguments --------- after: `Member` or `int` representing new user result: `Dict` mapping table - number of affected rows Returns ------- `discord.Embed` """ embed = self.embed(ctx=ctx, title=self.text.get("user", "move"), description=str(after)) result_items = [] for key, value in result.items(): result_items.append(f"{key}: `{value}`") embed.add_field( name=self.text.get("user", "result"), value="\n".join(result_items), ) return embed
class Comments(rubbercog.Rubbercog): """Manage user information""" def __init__(self, bot): super().__init__(bot) self.text = CogText("comments") @commands.guild_only() @commands.check(acl.check) @commands.group(name="comment") async def comments(self, ctx): """Manage comments on guild users""" await utils.send_help(ctx) @commands.check(acl.check) @comments.command(name="list") async def comments_list(self, ctx, user: Union[discord.Member, discord.User]): comments: List[Comment] = Comment.get(ctx.guild.id, user.id) if not len(comments): return await ctx.reply(self.text.get("list", "none")) def format_comment(comment: Comment) -> str: timestamp: str = comment.timestamp.strftime("%Y-%m-%d %H:%M") author: discord.Member = ctx.guild.get_member(comment.author_id) author_name: str = (f"{comment.author_id}" if author is None else discord.utils.escape_markdown( author.display_name)) text: str = "\n".join( [f"> {line}" for line in comment.text.split("\n")]) return f"**{author_name}**, {timestamp} (ID {comment.id}):\n{text}" response: str = "\n".join( [format_comment(comment) for comment in comments]) stubs: List[str] = utils.paginate(response) await ctx.reply(stubs[0]) if len(stubs) > 1: for stub in utils.paginate(response): await ctx.send(stub) @commands.check(acl.check) @comments.command(name="add") async def comments_add(self, ctx, user: Union[discord.Member, discord.User, int], *, text: str): Comment.add( guild_id=ctx.guild.id, author_id=ctx.author.id, user_id=user.id, text=text, ) await ctx.reply(self.text.get("add", "ok")) await self.event.sudo(ctx, f"Comment added on {user}.") @commands.check(acl.check) @comments.command(name="remove") async def comments_remove(self, ctx, id: int): success = Comment.remove(guild_id=ctx.guild.id, id=id) if not success: return await ctx.reply("remove", "none") await ctx.reply(self.text.get("remove", "ok")) await self.event.sudo(ctx, f"Comment with ID {id} removed.")
class Meme(rubbercog.Rubbercog): """Interact with users""" def __init__(self, bot): super().__init__(bot) self.text = CogText("meme") self.config = CogConfig("meme") self.fishing_pool = self.config.get("_fishing") @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def hug(self, ctx, *, target: Union[discord.Member, discord.Role] = None): """Hug someone! target: Discord user or role. If none, the bot will hug you. """ if target is None: hugger = self.bot.user hugged = ctx.author else: hugger = ctx.author hugged = target if type(hugged) == discord.Role: repo_i.add(ctx.guild.id, "hug", hugger.id, None) else: repo_i.add(ctx.guild.id, "hug", hugger.id, hugged.id) await ctx.send(emote.hug_right + (" **" + hugged.display_name + "**" if type(target) == discord.Member else " ***" + hugged.name + "***")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def whip(self, ctx, *, user: discord.Member = None): """Whip someone""" if user is None: whipper = self.bot.user whipped = ctx.author else: whipper = ctx.author whipped = user repo_i.add(ctx.guild.id, "whip", whipper.id, whipped.id) async with ctx.typing(): url = whipped.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_whip_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="whip.gif")) return @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def spank(self, ctx, *, user: discord.Member = None): """Spank someone""" if user is None: spanker = self.bot.user spanked = ctx.author else: spanker = ctx.author spanked = user repo_i.add(ctx.guild.id, "spank", spanker.id, spanked.id) async with ctx.typing(): url = spanked.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_spank_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="spank.gif")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def pet(self, ctx, *, member: discord.Member = None): """Pet someone! member: Discord user. If none, the bot will pet you. """ if member is None: petter = self.bot.user petted = ctx.author else: petter = ctx.author petted = member repo_i.add(ctx.guild.id, "pet", petter.id, petted.id) async with ctx.typing(): url = petted.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_pet_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=40, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="pet.gif")) return # this is a more intensive solution that creates non-transparent # background without it being glitched with BytesIO() as image_binary: image_utils.save_gif(frames, 30, image_binary) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="pet.gif")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def hyperpet(self, ctx, *, member: discord.Member = None): """Pet someone really hard member: Discord user. If none, the bot will hyperpet you. """ if member is None: petter = self.bot.user petted = ctx.author else: petter = ctx.author petted = member repo_i.add(ctx.guild.id, "hyperpet", petter.id, petted.id) async with ctx.typing(): url = petted.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_hyperpet_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="hyperpet.gif") ) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def bonk(self, ctx, *, member: discord.Member = None): """Bonk someone member: Discord user. If none, the bot will bonk you. """ if member is None: bonker = self.bot.user bonked = ctx.author else: bonker = ctx.author bonked = member repo_i.add(ctx.guild.id, "bonk", bonker.id, bonked.id) async with ctx.typing(): url = bonked.avatar_url_as(format="jpg") response = requests.get(url) avatar = Image.open(BytesIO(response.content)) frames = self.get_bonk_frames(avatar) with BytesIO() as image_binary: frames[0].save( image_binary, format="GIF", save_all=True, append_images=frames[1:], duration=30, loop=0, transparency=0, disposal=2, optimize=False, ) image_binary.seek(0) await ctx.send( file=discord.File(fp=image_binary, filename="bonk.gif")) @commands.guild_only() @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.command() async def slap(self, ctx, *, member: discord.Member = None): """Slap someone! member: Discord user. If none, the bot will slap you. """ if member is None: slapper = self.bot.user slapped = ctx.author else: slapper = ctx.author slapped = member options = ["つ", "づ", "ノ"] repo_i.add(ctx.guild.id, "slap", slapper.id, slapped.id) await ctx.send("**{}**{} {}".format( self.sanitise(slapper.display_name), random.choice(options), self.sanitise(slapped.display_name), )) @commands.guild_only() @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) @commands.command() async def relations(self, ctx, *, user: discord.User = None): """Get your information about hugs, pets, ...""" if user is None: user = ctx.author embed = self.embed( ctx=ctx, description=f"**{self.sanitise(user.display_name)}**\n" f"{self.text.get('relations_help')}", ) for action in ("hug", "pet", "hyperpet", "slap", "spank", "whip", "bonk"): lookup = repo_i.get_user_action(user.id, ctx.guild.id, action) if lookup[0] == 0 and lookup[1] == 0: continue value = self.text.get("value", gave=lookup[0], got=lookup[1]) embed.add_field(name=f"{config.prefix}{action}", value=value) await ctx.send(embed=embed) await utils.room_check(ctx) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command(aliases=["owo"]) async def uwu(self, ctx, *, message: str = None): """UWUize message""" if message is None: text = "OwO!" else: text = self.sanitise(self.uwuize(message), limit=1900, markdown=True) await ctx.send(f"**{self.sanitise(ctx.author.display_name)}**\n>>> " + text) await utils.delete(ctx.message) @commands.cooldown(rate=5, per=120, type=commands.BucketType.user) @commands.command(aliases=["rcase", "randomise"]) async def randomcase(self, ctx, *, message: str = None): """raNdOMisE cAsInG""" if message is None: text = "O.o" else: text = "" for letter in message: if letter.isalpha(): text += letter.upper() if random.choice( (True, False)) else letter.lower() else: text += letter text = self.sanitise(text[:1900], markdown=True) await ctx.send(f"**{self.sanitise(ctx.author.display_name)}**\n>>> " + text) await utils.delete(ctx.message) @commands.cooldown(rate=3, per=10, type=commands.BucketType.user) @commands.command() async def fish(self, ctx): """Go fishing!""" roll = random.uniform(0, 1) options = None for probabilty, harvest in self.fishing_pool.items(): if roll >= float(probabilty): options = harvest break else: return await ctx.send( self.text.get("fishing_fail", mention=ctx.author.mention)) await ctx.send(random.choice(options)) ## ## Logic ## @staticmethod def uwuize(string: str) -> str: # Adapted from https://github.com/PhasecoreX/PCXCogs/blob/master/uwu/uwu.py result = [] def uwuize_word(string: str) -> str: try: if string.lower()[0] == "m" and len(string) > 2: w = "W" if string[1].isupper() else "w" string = string[0] + w + string[1:] except Exception: # this is how we handle emojis pass string = string.replace("r", "w").replace("R", "W") string = string.replace("ř", "w").replace("Ř", "W") string = string.replace("l", "w").replace("L", "W") string = string.replace("?", "?" * random.randint(1, 3)) string = string.replace("'", ";" * random.randint(1, 3)) if string[-1] == ",": string = string[:-1] + "." * random.randint(2, 3) return string result = " ".join( [uwuize_word(s) for s in string.split(" ") if len(s)]) if result[-1] == "?": result += " UwU" if result[-1] == "!": result += " OwO" if result[-1] == ".": result = result[:-1] + "," * random.randint(2, 4) return result @staticmethod def round_image(frame_avatar: Image.Image) -> Image.Image: """Convert square avatar to circle""" frame_mask = Image.new("1", frame_avatar.size, 0) draw = ImageDraw.Draw(frame_mask) draw.ellipse((0, 0) + frame_avatar.size, fill=255) frame_avatar.putalpha(frame_mask) return frame_avatar @staticmethod def get_pet_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the pet""" frames = [] width, height = 148, 148 vertical_offset = (0, 0, 0, 0, 1, 2, 3, 4, 5, 4, 3, 2, 2, 1, 0) frame_avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(14): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) hand = Image.open(f"data/meme/pet/{img}.png") frame.paste(frame_avatar, (35, 25 + vertical_offset[i]), frame_avatar) frame.paste(hand, (10, 5), hand) frames.append(frame) return frames @staticmethod def get_hyperpet_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the hyperpet""" frames = [] width, height = 148, 148 vertical_offset = (0, 1, 2, 3, 1, 0) avatar = image_utils.round_image(avatar.resize((100, 100))) avatar_pixels = np.array(avatar) git_hash = int(utils.git_get_hash(), 16) for i in range(6): deform_hue = git_hash % 100**(i + 1) // 100**i / 100 frame_avatar = Image.fromarray( image_utils.shift_hue(avatar_pixels, deform_hue)) img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) hand = Image.open(f"data/meme/hyperpet/{img}.png") frame.paste(frame_avatar, (35, 25 + vertical_offset[i]), frame_avatar) frame.paste(hand, (10, 5), hand) frames.append(frame) return frames @staticmethod def get_bonk_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the bonk""" frames = [] width, height = 200, 170 deformation = (0, 0, 0, 5, 10, 20, 15, 5) avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(8): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) bat = Image.open(f"data/meme/bonk/{img}.png") frame_avatar = avatar.resize((100, 100 - deformation[i])) frame.paste(frame_avatar, (80, 60 + deformation[i]), frame_avatar) frame.paste(bat, (10, 5), bat) frames.append(frame) return frames @staticmethod def get_whip_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the whip""" frames = [] width, height = 250, 150 deformation = (0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 9, 6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) translation = (0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0) avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(26): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) whip_frame = Image.open(f"data/meme/whip/{img}.png").resize( (150, 150)) frame_avatar = avatar.resize((100 - deformation[i], 100)) frame.paste(frame_avatar, (135 + deformation[i] + translation[i], 25), frame_avatar) frame.paste(whip_frame, (0, 0), whip_frame) frames.append(frame) return frames @staticmethod def get_spank_frames(avatar: Image.Image) -> List[Image.Image]: """Get frames for the spank""" frames = [] width, height = 200, 120 deformation = (4, 2, 1, 0, 0, 0, 0, 3) avatar = image_utils.round_image(avatar.resize((100, 100))) for i in range(8): img = "%02d" % (i + 1) frame = Image.new("RGBA", (width, height), (54, 57, 63, 1)) spoon = Image.open(f"data/meme/spank/{img}.png").resize((100, 100)) frame_avatar = avatar.resize( (100 + 2 * deformation[i], 100 + 2 * deformation[i])) frame.paste(spoon, (10, 15), spoon) frame.paste(frame_avatar, (80 - deformation[i], 10 - deformation[i]), frame_avatar) frames.append(frame) return frames
def __init__(self, bot): super().__init__(bot) self.text = CogText("random")
class Random(rubbercog.Rubbercog): """Pick, flip, roll dice""" def __init__(self, bot): super().__init__(bot) self.text = CogText("random") @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user) @commands.check(check.is_verified) @commands.command() async def pick(self, ctx, *args): """"Pick an option""" for i, arg in enumerate(args): if arg.endswith("?"): args = args[i + 1:] break if not len(args): return option = self.sanitise(random.choice(args), limit=50) if option is not None: await ctx.send( self.text.get("answer", mention=ctx.author.mention, option=option)) await utils.room_check(ctx) @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user) @commands.check(check.is_verified) @commands.command() async def flip(self, ctx): """Yes/No""" option = random.choice(self.text.get("flip")) await ctx.send( self.text.get("answer", mention=ctx.author.mention, option=option)) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user) @commands.check(check.is_verified) @commands.command() async def random(self, ctx, first: int, second: int = None): """Pick number from interval""" if second is None: second = 0 if first > second: first, second = second, first option = random.randint(first, second) await ctx.send( self.text.get("answer", mention=ctx.author.mention, option=option)) await utils.room_check(ctx) @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel) @commands.check(check.is_verified) @commands.command(aliases=["unsplash"]) async def picsum(self, ctx, *, seed: str = None): """Get random image from picsum.photos""" size = "900/600" url = "https://picsum.photos/" if seed: url += "seed/" + seed + "/" url += f"{size}.jpg?random={ctx.message.id}" # we cannot use the URL directly, because embed will contain other image than its thumbnail image = requests.get(url) if image.status_code != 200: return await ctx.send(f"E{image.status_code}") # get image info # example url: https://i.picsum.photos/id/857/600/360.jpg?hmac=..... image_id = image.url.split("/id/", 1)[1].split("/")[0] image_info = requests.get(f"https://picsum.photos/id/{image_id}/info") try: image_url = image_info.json()["url"] except: image_url = discord.Embed.Empty embed = self.embed(ctx=ctx, title=discord.Embed.Empty, description=image_url, footer=seed) embed.set_image(url=image.url) await ctx.send(embed=embed) await utils.room_check(ctx)
def __init__(self, bot): super().__init__(bot) self.text = CogText("comments")
class Errors(rubbercog.Rubbercog): def __init__(self, bot): super().__init__(bot) self.text = CogText("errors") @commands.Cog.listener() async def on_command_error(self, ctx, error): # noqa: C901 """Handle errors""" if hasattr(ctx.command, "on_error") or hasattr(ctx.command, "on_command_error"): return error = getattr(error, "original", error) throw_error = await self._send_exception_message(ctx, error) if not throw_error: return # display error message await self.output.error(ctx, "", error) output = self._format_output(ctx, error) print(output) # send traceback to dedicated channel channel_stdout = self.bot.get_channel(config.get("channels", "stdout")) output = list(output[0 + i:1960 + i] for i in range(0, len(output), 1960)) sent = [] for message in output: m = await channel_stdout.send("```\n{}```".format(message)) sent.append(m) # send notification to botdev await self._send_notification(ctx, output, error, sent[0].jump_url) ## ## Logic ## async def _send_exception_message(self, ctx: commands.Context, error: Exception) -> bool: """Return True if error should be thrown""" # fmt: off if isinstance(error, acl_repo.ACLException): await self.output.error( ctx, self.text.get("acl", type(error).__name__, error=str(error))) return False # cog exceptions are handled in their cogs if isinstance(error, rubbercog.RubbercogException): if type(error) is not rubbercog.RubbercogException: return False await self.output.error(ctx, self.text.get("RubbercogException"), error) return False if type(error) == commands.CommandNotFound: return # Exceptions with parameters if type(error) == commands.MissingRequiredArgument: await self.output.warning( ctx, self.text.get("MissingRequiredArgument", param=error.param.name)) return False if type(error) == commands.CommandOnCooldown: time = utils.seconds2str(error.retry_after) await self.output.warning( ctx, self.text.get("CommandOnCooldown", time=time)) return False if type(error) == commands.MaxConcurrencyReached: await self.output.warning( ctx, self.text.get("MaxConcurrencyReached", num=error.number, per=error.per.name)) return False if type(error) == commands.MissingRole: # TODO Is !r OK, or should we use error.missing_role.name? role = f"`{error.missing_role!r}`" await self.output.warning(ctx, self.text.get("MissingRole", role=role)) return False if type(error) == commands.BotMissingRole: role = f"`{error.missing_role!r}`" await self.output.error(ctx, self.text.get("BotMissingRole", role=role)) return False if type(error) == commands.MissingAnyRole: roles = ", ".join(f"`{r!r}`" for r in error.missing_roles) await self.output.warning( ctx, self.text.get("MissingAnyRole", roles=roles)) return False if type(error) == commands.BotMissingAnyRole: roles = ", ".join(f"`{r!r}`" for r in error.missing_roles) await self.output.error( ctx, self.text.get("BotMissingAnyRole", roles=roles)) return False if type(error) == commands.MissingPermissions: perms = ", ".join(f"`{p}`" for p in error.missing_perms) await self.output.warning( ctx, self.text.get("MissingPermissions", perms=perms)) return False if type(error) == commands.BotMissingPermissions: perms = ", ".join(f"`{p}`" for p in error.missing_perms) await self.output.error( ctx, self.text.get("BotMissingPermissions", perms=perms)) return False if type(error) == commands.BadUnionArgument: await self.output.warning( ctx, self.text.get("BadUnionArgument", param=error.param.name)) return False if type(error) == commands.BadBoolArgument: await self.output.warning( ctx, self.text.get("BadBoolArgument", arg=error.argument)) return False # All cog-related errors if isinstance(error, smtplib.SMTPException): await self.console.error(ctx, "Could not send e-mail", error) await ctx.send( self.text.get("SMTPException", name=type(error).__name__)) return False if type(error) == commands.ExtensionFailed: await self.output.error( ctx, self.text.get( type(error).__name__, extension=f"{error.name!r}", error_name=error.original.__class__.__name__, error=str(error.original), )) return False if isinstance(error, commands.ExtensionError): await self.output.critical( ctx, self.text.get(type(error).__name__, extension=f"{error.name!r}")) return False # The rest of client exceptions if isinstance(error, commands.CommandError) or isinstance( error, discord.ClientException): await self.output.warning(ctx, self.text.get(type(error).__name__)) return False # DiscordException, non-critical errors if type(error) in ( discord.errors.NoMoreItems, discord.errors.HTTPException, discord.errors.Forbidden, discord.errors.NotFound, ): await self.output.error(ctx, self.text.get(type(error).__name__)) await self.console.error(ctx, type(error).__name__, error) return False # DiscordException, critical errors if type(error) in (discord.errors.DiscordException, discord.errors.GatewayNotFound): await self.output.error(ctx, self.text.get(type(error).__name__)) # Database if isinstance(error, sqlalchemy.exc.SQLAlchemyError): error_name = ".".join( [type(error).__module__, type(error).__name__]) await self.output.critical(ctx, error_name) await self.console.critical(ctx, "Database error", error) await self.event.user( ctx, f"Database reported`{error_name}`. The session may be invalidated <@{config.admin_id}>", escape_markdown=False, ) return False # fmt: on return True def _format_output(self, ctx: commands.Context, error) -> str: if isinstance(ctx.channel, discord.TextChannel): location = f"{ctx.guild.name}/{ctx.channel.name} ({ctx.channel.id})" else: location = type(ctx.channel).__name__ output = "{command} by {user} in {location}\n".format( command=config.prefix + ctx.command.qualified_name, user=str(ctx.author), location=location, ) output += "".join( traceback.format_exception(type(error), error, error.__traceback__)) return output async def _send_notification(self, ctx: commands.Context, output: str, error: Exception, traceback_url: str): channel = self.bot.get_channel(config.channel_botdev) embed = self.embed(ctx=ctx, color=discord.Color.from_rgb(255, 0, 0)) # fmt: off footer = "{user} in {channel}".format( user=str(ctx.author), channel=ctx.channel.name if isinstance(ctx.channel, discord.TextChannel) else type( ctx.channel).__name__) embed.set_footer(text=footer, icon_url=ctx.author.avatar_url) stack = output[-1] if len(stack) > 255: stack = "…" + stack[-255:] embed.add_field( name=type(error).__name__, value=f"```{stack}```", inline=False, ) embed.add_field(name="Traceback", value=traceback_url, inline=False) await channel.send(embed=embed)
class Librarian(rubbercog.Rubbercog): """Knowledge and information based commands""" # TODO Move czech strings to text.default.json def __init__(self, bot): super().__init__(bot) self.config = CogConfig("librarian") self.text = CogText("librarian") @commands.command(aliases=["svátek"]) async def svatek(self, ctx): url = f"http://svatky.adresa.info/json?date={date.today().strftime('%d%m')}" res = await utils.fetch_json(url) names = [] for i in res: names.append(i["name"]) if len(names): await ctx.send( self.text.get("nameday", "cs", name=", ".join(names))) else: await ctx.send(self.text.get("nameday", "cs0")) @commands.command(aliases=["sviatok"]) async def meniny(self, ctx): url = f"http://svatky.adresa.info/json?lang=sk&date={date.today().strftime('%d%m')}" res = await utils.fetch_json(url) names = [] for i in res: names.append(i["name"]) if len(names): await ctx.send( self.text.get("nameday", "sk", name=", ".join(names))) else: await ctx.send(self.text.get("nameday", "sk0")) @commands.command(aliases=["tyden", "týden", "tyzden", "týždeň"]) async def week(self, ctx: commands.Context): """See if the current week is odd or even""" cal_week = date.today().isocalendar()[1] stud_week = cal_week - self.config.get("starting_week") + 1 even, odd = self.text.get("week", "even"), self.text.get("week", "odd") cal_type = even if cal_week % 2 == 0 else odd stud_type = even if stud_week % 2 == 0 else odd embed = self.embed(ctx=ctx) embed.add_field( name=self.text.get("week", "calendar"), value="{} ({})".format(cal_type, cal_week), ) if 0 < stud_week <= self.config.get("total_weeks"): embed.add_field( name=self.text.get("week", "study"), value=str(stud_week), ) await ctx.send(embed=embed) await utils.delete(ctx) await utils.room_check(ctx) @commands.command(aliases=["počasí", "pocasi", "počasie", "pocasie"]) async def weather(self, ctx, *, place: str = "Brno"): token = self.config.get("weather_token") place = place[:100] if "&" in place: return await ctx.send(self.text.get("weather", "place_not_found")) url = ("https://api.openweathermap.org/data/2.5/weather?q=" + place + "&units=metric&lang=cz&appid=" + token) res = await utils.fetch_json(url) """ Example response { "coord":{ "lon":16.61, "lat":49.2 }, "weather":[ { "id":800, "temp_maixn":"Clear", "description":"jasno", "icon":"01d" } ], "base":"stations", "main":{ "temp":21.98, "feels_like":19.72, "temp_min":20.56, "temp_max":23, "pressure":1013, "humidity":53 }, "visibility":10000, "wind":{ "speed":4.1, "deg":50 }, "clouds":{ "all":0 }, "dt":1595529518, "sys":{ "type":1, "id":6851, "country":"CZ", "sunrise":1595474051, "sunset":1595529934 }, "timezone":7200, "id":3078610, "name":"Brno", "cod":200 } """ if str(res["cod"]) == "404": return await ctx.send(self.text.get("weather", "place_not_found")) elif str(res["cod"]) == "401": return await ctx.send(self.text.get("weather", "token")) elif str(res["cod"]) != "200": return await ctx.send( self.text.get("weather", "place_error", message=res["message"])) title = res["weather"][0]["description"] description = (self.text.get("weather", "description", name=res["name"], country=res["sys"]["country"]) if "country" in res["sys"] else self.text.get( "weather", "description_short", name=res["name"])) if description.endswith("CZ"): description = description[:-4] embed = self.embed(ctx=ctx, title=title[0].upper() + title[1:], description=description) embed.set_thumbnail(url="https://openweathermap.org/img/w/{}.png". format(res["weather"][0]["icon"])) embed.add_field( name=self.text.get("weather", "temperature"), value=self.text.get( "weather", "temperature_value", real=round(res["main"]["temp"], 1), feel=round(res["main"]["feels_like"], 1), ) + "\n" + self.text.get( "weather", "temperature_minmax", min=round(res["main"]["temp_min"], 1), max=round(res["main"]["temp_max"], 1), ), inline=False, ) embed.add_field( name=self.text.get("weather", "humidity"), value=str(res["main"]["humidity"]) + " %", ) embed.add_field( name=self.text.get("weather", "clouds"), value=(str(res["clouds"]["all"]) + " %"), ) if "visibility" in res: if res["visibility"] == 10000: value = self.text.get("weather", "visibility_max") else: value = f"{res['visibility']/1000} km" embed.add_field( name=self.text.get("weather", "visibility"), value=value, ) embed.add_field(name=self.text.get("weather", "wind"), value=f"{res['wind']['speed']} m/s") await utils.send(ctx, embed=embed) await utils.room_check(ctx) @commands.command(aliases=["b64"]) async def base64(self, ctx, direction: str, *, data: str): """Get base64 data direction: [encode, e, -e; decode, d, -d] text: string (under 1000 characters) """ if data is None or not len(data): return await utils.send_help(ctx) data = data[:1000] if direction in ("encode", "e", "-e"): direction = "encode" result = base64.b64encode(data.encode("utf-8")).decode("utf-8") elif direction in ("decode", "d", "-d"): direction = "decode" try: result = base64.b64decode(data.encode("utf-8")).decode("utf-8") except Exception as e: return await ctx.send(f"> {e}") else: return await utils.send_help(ctx) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**base64 {direction}** ({quote}):\n> ```{result}```") await utils.room_check(ctx) @commands.command() async def hashlist(self, ctx): """Get list of available hash functions""" result = "**hashlib**\n" result += "> " + " ".join(sorted(hashlib.algorithms_available)) await ctx.send(result) @commands.command() async def hash(self, ctx, fn: str, *, data: str): """Get hash function result Run hashlist command to see available algorithms """ if fn in hashlib.algorithms_available: result = hashlib.new(fn, data.encode("utf-8")).hexdigest() else: return await ctx.send(self.text.get("invalid_hash")) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**{fn}** ({quote}):\n> ```{result}```") @commands.command(aliases=["maclookup"]) async def macaddress(self, ctx, mac: str): """Get information about MAC address""" apikey = self.config.get("maclookup_token") if apikey == 0: return await self.output.error( ctx, self.text.get("maclookup", "no_token"), ) if "&" in mac or "?" in mac: return await self.output.error( ctx, self.text.get("maclookup", "bad_mac", mention=ctx.author.mention), ) url = f"https://api.maclookup.app/v2/macs/{mac}?format=json&apiKey={apikey}" res = await utils.fetch_json(url) if res["success"] is False: embed = self.embed( ctx=ctx, title=self.text.get("maclookup", "error", errcode=res["errorCode"]), description=res["error"], footer="maclookup.app", ) return await ctx.send(embed=embed) if res["found"] is False: embed = self.embed( ctx=ctx, title=self.text.get("maclookup", "error", errcode="404"), description=self.text.get("maclookup", "not_found"), footer="maclookup.app", ) return await ctx.send(embed=embed) embed = self.embed(ctx=ctx, title=res["macPrefix"], footer="maclookup.app") embed.add_field( name=self.text.get("maclookup", "company"), value=res["company"], inline=False, ) embed.add_field(name=self.text.get("maclookup", "country"), value=res["country"]) block = f"`{res['blockStart']}`" if res["blockStart"] != res["blockEnd"]: block += f"\n`{res['blockEnd']}`" embed.add_field(name=self.text.get("maclookup", "block"), value=f'`{res["blockStart"]}`') await ctx.send(embed=embed) @commands.cooldown(rate=2, per=20, type=commands.BucketType.user) # The API has limit of 45 requests per minute @commands.cooldown(rate=45, per=55, type=commands.BucketType.default) @commands.command(aliases=["iplookup"]) async def ipaddress(self, ctx, query: str): """Get information about an IP address or a domain name""" if "&" in query or "?" in query or not len(query): return await self.output.error( ctx, self.text.get("iplookup", "bad_query", mention=ctx.author.mention), ) url = ( f"http://ip-api.com/json/{query}" "?fields=query,status,message,country,regionName,city,lat,lon,isp,org" ) res = await utils.fetch_json(url) # TODO The API states that we should be listening for the `X-Rl` header. # If it is `0`, we must stop for `X-ttl` seconds. # https://ip-api.com/docs/api:json if res["status"] == "fail": embed = self.embed( ctx=ctx, title=self.text.get("iplookup", "error"), description="`" + res["message"] + "`", footer="ip-api.com", ) return await ctx.send(embed=embed) embed = self.embed(ctx=ctx, title=res["query"], footer="ip-api.com") embed.add_field( name=res["city"], value=f"{res['regionName']}, {res['country']}", inline=False, ) embed.add_field( name=self.text.get("iplookup", "geo"), value=f"{res['lon']}, {res['lat']}", ) embed.add_field( name=self.text.get("iplookup", "org"), value=res["org"], ) embed.add_field( name=self.text.get("iplookup", "isp"), value=res["isp"], ) await ctx.send(embed=embed)
class Roles(rubbercog.Rubbercog): """Manage roles and subjects""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("roles") self.text = CogText("roles") self.limit_programmes = {} self.limit_interests = {} ## ## Getters ## def getLimitProgrammes(self, channel: discord.TextChannel) -> discord.Role: gid = str(channel.guild.id) if gid not in self.limit_programmes: self.limit_programmes[gid] = discord.utils.get( channel.guild.roles, name="---PROGRAMMES") return self.limit_programmes[gid] def getLimitInterests(self, channel: discord.TextChannel) -> discord.Role: gid = str(channel.guild.id) if gid not in self.limit_interests: self.limit_interests[gid] = discord.utils.get(channel.guild.roles, name="---INTERESTS") return self.limit_interests[gid] ## ## Listeners ## @commands.Cog.listener() async def on_message(self, message): """Listen for react-to-role message""" if not isinstance(message.channel, discord.TextChannel): return if message.channel.id not in self.config.get("r2r_channels"): return emote_channel_list = await self._message_to_tuple_list(message) if emote_channel_list is None: # do not throw errors if nothing is found return for emote_channel in emote_channel_list: try: await message.add_reaction(emote_channel[0]) except (discord.errors.Forbidden, discord.errors.HTTPException): continue @commands.Cog.listener() async def on_raw_message_edit(self, payload: discord.RawMessageUpdateEvent): """Listen for react-to-role message changes""" if payload.channel_id not in self.config.get("r2r_channels"): return message_channel = self.bot.get_channel(payload.channel_id) message = await message_channel.fetch_message(payload.message_id) # make a list of current emotes emote_channel_list = await self._message_to_tuple_list(message) for emote_channel in emote_channel_list: try: await message.add_reaction(emote_channel[0]) except (discord.errors.Forbidden, discord.errors.HTTPException): continue @commands.Cog.listener() async def on_raw_reaction_add(self, payload): # extract data from payload payload = await self._reaction_payload_to_tuple(payload) if payload is None: return channel, message, member, emoji = payload emote_channel_list = await self._message_to_tuple_list(message) result = None for emote_channel in emote_channel_list: if str(emoji) == emote_channel[0]: # try both subject and role subject = await self._get_subject(channel, emote_channel[1]) if subject is not None: result = await self._subject_add(message.channel, member, subject) break role = await self._get_role(channel, emote_channel[1]) if role is not None: result = await self._role_add(message.channel, member, role) break else: # another emote was added result = None if not result: try: await message.remove_reaction(emoji, member) except Exception: pass @commands.Cog.listener() async def on_raw_reaction_remove(self, payload): # extract data from payload payload = await self._reaction_payload_to_tuple(payload) if payload is None: return channel, message, member, emoji = payload emote_channel_list = await self._message_to_tuple_list(message) for emote_channel in emote_channel_list: if str(emoji) == emote_channel[0]: # try both subject and role subject = await self._get_subject(channel, emote_channel[1]) if subject is not None: await self._subject_remove(message.channel, member, subject) break role = await self._get_role(channel, emote_channel[1]) if role is not None: await self._role_remove(message.channel, member, role) break ## ## Helper functions ## async def _get_subject(self, location, shortcut: str) -> discord.TextChannel: db_subject = repo_s.get(shortcut) if db_subject is not None: return discord.utils.get(location.guild.text_channels, name=shortcut) return async def _get_role(self, location, role: str) -> discord.Role: return discord.utils.get(location.guild.roles, name=role) async def _message_to_tuple_list(self, message: discord.Message) -> list: """Return (emote, channel/role) list""" # preprocess message content content = message.content.replace("*", "").replace("_", "").replace("#", "") try: content = content.rstrip().split("\n") except ValueError: await message.channel.send(self.text.get("role_help")) return # check every line result = [] for line in content: try: line_ = line.split(" ") emote = line_[0] target = line_[1] if "<#" in emote: # custom emote, get it's ID emote = int(emote.replace("<#", "").replace(">", "")) result.append((emote, target)) except Exception: # do not send errors if message is in #add-* channel if message.channel.id in self.config.get("r2r_channels"): return await self._send( message.channel, self.text.fill("invalid_role_line", line=self.sanitise(line, limit=50)), ) return return result async def _reaction_payload_to_tuple( self, payload: discord.RawMessageUpdateEvent) -> tuple: """Return (channel, message, member, emoji) or None""" # channel channel = self.bot.get_channel(payload.channel_id) if not isinstance(channel, discord.TextChannel): return # message try: message = await channel.fetch_message(payload.message_id) except discord.NotFound: return # halt if not react-to-role message if channel.id not in self.config.get("r2r_channels"): return # member member = message.guild.get_member(payload.user_id) if member.bot: return # emoji if payload.emoji.is_custom_emoji(): emoji = self.bot.get_emoji(payload.emoji.id) or payload.emoji else: emoji = payload.emoji.name return channel, message, member, emoji def _get_teacher_channel( self, subject: discord.TextChannel) -> discord.TextChannel: return discord.utils.get( subject.guild.text_channels, name=subject.name + config.get("channels", "teacher suffix"), ) ## ## Logic ## async def _subject_add( self, source: discord.TextChannel, member: discord.Member, channel: discord.TextChannel, ) -> bool: # check permission for subject_role in self.config.get("subject_roles"): if subject_role in [r.id for r in member.roles]: break else: # they do not have neccesary role await self._send( source, self.text.get("deny_subject", mention=member.mention)) return False await channel.set_permissions(member, view_channel=True) teacher_channel = self._get_teacher_channel(channel) if teacher_channel is not None: await teacher_channel.set_permissions(member, view_channel=True) return True async def _subject_remove( self, source: discord.TextChannel, member: discord.Member, channel: discord.TextChannel, ): # we do not need to check for permissions await channel.set_permissions(member, overwrite=None) teacher_channel = self._get_teacher_channel(channel) if teacher_channel is not None: await teacher_channel.set_permissions(member, overwrite=None) async def _role_add(self, channel: discord.TextChannel, member: discord.Member, role: discord.Role) -> bool: if role < self.getLimitProgrammes( channel) and role > self.getLimitInterests(channel): # role is programme, check if user has permission for programme_role in self.config.get("programme_roles"): if programme_role in [r.id for r in member.roles]: break else: await self._send( channel, self.text.get("deny_programme", mention=member.mention)) return False # check if user already doesn't have some programme role for user_role in member.roles: # fmt: off if user_role < self.getLimitProgrammes(channel) \ and user_role > self.getLimitInterests(channel): await self._send( channel, self.text.get("deny_second_programme", mention=member.mention), ) return False # fmt: on elif role < self.getLimitInterests(channel): # role is below interests limit, continue pass else: # role is limit itself or something above programmes await self._send( channel, self.text.get("deny_high_role", mention=member.mention)) return False await member.add_roles(role) # optionally, hide channel if channel.id in self.config.get("r2h_channels"): await channel.set_permissions(member, read_messages=False) return True async def _role_remove(self, channel: discord.TextChannel, member: discord.Member, role: discord.Role): if role < self.getLimitProgrammes( channel) and role > self.getLimitInterests(channel): # role is programme, check if user has permission for programme_role in self.config.get("programme_roles"): if programme_role in [r.id for r in member.roles]: break else: await self._send( channel, self.text.get("deny_programme", mention=member.mention)) return elif role < self.getLimitInterests(channel): # role is below interests limit, continue pass else: # role is limit itself or something above programmes return await self._send( channel, self.text.get("deny_high_role", mention=member.mention)) await member.remove_roles(role) async def _send(self, channel: discord.TextChannel, text: str): if channel.id in self.config.get("r2r_channels"): return await channel.send(text, delete_after=config.get("delay", "user error"))
def __init__(self, bot): super().__init__(bot) self.config = CogConfig("librarian") self.text = CogText("librarian")
class Librarian(rubbercog.Rubbercog): """Knowledge and information based commands""" # TODO Move czech strings to text.default.json def __init__(self, bot): super().__init__(bot) self.config = CogConfig("librarian") self.text = CogText("librarian") @commands.command(aliases=["svátek"]) async def svatek(self, ctx): url = f"http://svatky.adresa.info/json?date={date.today().strftime('%d%m')}" res = await self.fetch_json(url) names = [] for i in res: names.append(i["name"]) await ctx.send(self.text.get("nameday", "cs", name=", ".join(names))) @commands.command(aliases=["sviatok"]) async def meniny(self, ctx): url = f"http://svatky.adresa.info/json?lang=sk&date={date.today().strftime('%d%m')}" res = await self.fetch_json(url) names = [] for i in res: names.append(i["name"]) await ctx.send(self.text.get("nameday", "sk", name=", ".join(names))) @commands.command(aliases=["tyden", "týden", "tyzden", "týždeň"]) async def week(self, ctx: commands.Context): """See if the current week is odd or even""" cal_week = date.today().isocalendar()[1] stud_week = cal_week - self.config.get("starting_week") even, odd = self.text.get("week", "even"), self.text.get("week", "odd") cal_type = even if cal_week % 2 == 0 else odd stud_type = even if stud_week % 2 == 0 else odd embed = self.embed(ctx=ctx) embed.add_field( name=self.text.get("week", "study"), value="{} ({})".format(stud_type, stud_week), ) embed.add_field( name=self.text.get("week", "calendar"), value="{} ({})".format(cal_type, cal_week), ) await ctx.send(embed=embed) await utils.delete(ctx) await utils.room_check(ctx) @commands.command(aliases=["počasí", "pocasi", "počasie", "pocasie"]) async def weather(self, ctx, *, place: str = "Brno"): token = self.config.get("weather_token") place = place[:100] if "&" in place: return await ctx.send(self.text.get("weather", "place_not_found")) url = ("https://api.openweathermap.org/data/2.5/weather?q=" + place + "&units=metric&lang=cz&appid=" + token) res = await self.fetch_json(url) """ Example response { "coord":{ "lon":16.61, "lat":49.2 }, "weather":[ { "id":800, "temp_maixn":"Clear", "description":"jasno", "icon":"01d" } ], "base":"stations", "main":{ "temp":21.98, "feels_like":19.72, "temp_min":20.56, "temp_max":23, "pressure":1013, "humidity":53 }, "visibility":10000, "wind":{ "speed":4.1, "deg":50 }, "clouds":{ "all":0 }, "dt":1595529518, "sys":{ "type":1, "id":6851, "country":"CZ", "sunrise":1595474051, "sunset":1595529934 }, "timezone":7200, "id":3078610, "name":"Brno", "cod":200 } """ if str(res["cod"]) == "404": return await ctx.send(self.text.get("weather", "place_not_found")) elif str(res["cod"]) == "401": return await ctx.send(self.text.get("weather", "token")) elif str(res["cod"]) != "200": return await ctx.send( self.text.get("weather", "place_error", message=res["message"])) title = res["weather"][0]["description"] description = self.text.get("weather", "description", name=res["name"], country=res["sys"]["country"]) if description.endswith("CZ"): description = description[:-4] embed = self.embed(ctx=ctx, title=title[0].upper() + title[1:], description=description) embed.set_thumbnail(url="https://openweathermap.org/img/w/{}.png". format(res["weather"][0]["icon"])) embed.add_field( name=self.text.get("weather", "temperature"), value=self.text.get( "weather", "temperature_value", real=round(res["main"]["temp"], 1), feel=round(res["main"]["feels_like"], 1), ), inline=False, ) embed.add_field( name=self.text.get("weather", "humidity"), value=str(res["main"]["humidity"]) + " %", ) embed.add_field( name=self.text.get("weather", "clouds"), value=(str(res["clouds"]["all"]) + " %"), ) if "visibility" in res: embed.add_field( name=self.text.get("weather", "visibility"), value=f"{int(res['visibility']/1000)} km", ) embed.add_field(name=self.text.get("weather", "wind"), value=f"{res['wind']['speed']} m/s") await utils.send(ctx, embed=embed) await utils.room_check(ctx) @commands.command(aliases=["b64"]) async def base64(self, ctx, direction: str, *, data: str): """Get base64 data direction: [encode, e, -e; decode, d, -d] text: string (under 1000 characters) """ if data is None or not len(data): return await utils.send_help(ctx) data = data[:1000] if direction in ("encode", "e", "-e"): direction = "encode" result = base64.b64encode(data.encode("utf-8")).decode("utf-8") elif direction in ("decode", "d", "-d"): direction = "decode" try: result = base64.b64decode(data.encode("utf-8")).decode("utf-8") except Exception as e: return await ctx.send(f"> {e}") else: return await utils.send_help(ctx) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**base64 {direction}** ({quote}):\n> ```{result}```") await utils.room_check(ctx) @commands.command() async def hashlist(self, ctx): """Get list of available hash functions""" result = "**hashlib**\n" result += "> " + " ".join(sorted(hashlib.algorithms_available)) await ctx.send(result) @commands.command() async def hash(self, ctx, fn: str, *, data: str): """Get hash function result Run hashlist command to see available algorithms """ if fn in hashlib.algorithms_available: result = hashlib.new(fn, data.encode("utf-8")).hexdigest() else: return await ctx.send(self.text.get("invalid_hash")) quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "") await ctx.send(f"**{fn}** ({quote}):\n> ```{result}```") async def fetch_json(self, url: str) -> dict: """Fetch data from a URL and return a dict""" async with aiohttp.ClientSession() as cs: async with cs.get(url) as r: return await r.json()
def __init__(self, bot): super().__init__(bot) self.text = CogText("errors")
class Animals(rubbercog.Rubbercog): """Private zone""" def __init__(self, bot): super().__init__(bot) self.config = CogConfig("animals") self.text = CogText("animals") self.channel = None self.role = None # Because the API doesn't return the avatar resource immediately, # sometimes nothing happens, because the client caches the 404 # response (?). This is an attempt to counter that. self.check_delay = 10 def getChannel(self): if self.channel is None: self.channel = self.bot.get_channel(self.config.get("channel")) return self.channel def getRole(self): if self.role is None: self.role = self.getChannel().guild.get_role( self.config.get("role")) return self.role ## ## Commands ## @commands.check(acl.check) @commands.command() async def animal(self, ctx, member: discord.Member): """Send vote embed""" await self.check(member, "manual") ## ## Listeners ## @commands.Cog.listener() async def on_user_update(self, before: discord.User, after: discord.User): # only act if user is verified member = self.getGuild().get_member(after.id) if member is None: return # only act if user is verified if self.getVerifyRole() not in member.roles: return # only act if user has changed their avatar if before.avatar_url == after.avatar_url: return await asyncio.sleep(self.check_delay) await self.check(after, "on_user_update") @commands.Cog.listener() async def on_member_update(self, before: discord.Member, after: discord.Member): # only act if the user has been verified verify = self.getVerifyRole() if not (verify not in before.roles and verify in after.roles): return # only act if their avatar is not default if after.avatar_url == after.default_avatar_url: await self.console.debug(f"{after} verified", "Not an animal (default avatar).") return # lookup user timestamp, only allow new verifications db_user = repo_u.get(after.id) if db_user is not None and db_user.status == "verified": db_user = repo_u.get(after.id) timestamp = datetime.strptime(db_user.changed, "%Y-%m-%d %H:%M:%S") now = datetime.now() if (now - timestamp).total_seconds() > 5: # this was probably temporary unverify, they have been checked before await self.console.debug(f"{after} reverified", "Skipping (unverify).") return await asyncio.sleep(self.check_delay) await self.check(after, "on_member_update") @commands.Cog.listener() async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): """Vote""" if payload.channel_id != self.getChannel().id: return if payload.member.bot: return try: message = await self.getChannel().fetch_message(payload.message_id) except Exception: message = None if (message is None or len(message.embeds) != 1 or message.embeds[0].title != self.text.get("title")): return if str(payload.emoji) not in ("☑️", "❎"): return await message.remove_reaction(payload.emoji, payload.member) animal_id = int(message.embeds[0].description.split(" | ")[1]) if animal_id == payload.member.id: return await message.remove_reaction(payload.emoji, payload.member) try: animal = await self.getChannel().guild.fetch_member(animal_id) except discord.errors.NotFound: animal = None if animal is None: await self.console.error( "animals", f"Could not find member {animal_id}: abort.") return await utils.delete(message) # delete if the user has changed their avatar since the embed creation if str(message.embeds[0].image.url) != str(animal.avatar_url): await self.console.debug( animal, "Avatar has changed since. Vote aborted.") return await utils.delete(message) animal_avatar_url = animal.avatar_url_as(format="jpg") animal_avatar_data = requests.get(animal_avatar_url) animal_avatar = Image.open(BytesIO(animal_avatar_data.content)) animal_avatar_file = tempfile.TemporaryFile() for r in message.reactions: if r.emoji == "☑️" and r.count > self.config.get("limit"): avatar_result: Image.Image = Animals.add_border( animal_avatar, 3, True) avatar_result.save(animal_avatar_file, "png") animal_avatar_file.seek(0) if self.getRole() in animal.roles: # member is an animal and has been before await self.getChannel().send( self.text.get( "result", "yes_yes", nickname=self.sanitise(animal.display_name), ), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) else: # member is an animal and has not been before try: await animal.add_roles(self.getRole()) await self.getChannel().send( self.text.get("result", "no_yes", mention=animal.mention), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) except Exception as e: await self.console.error(message, "Could not add animal", e) break elif r.emoji == "❎" and r.count > self.config.get("limit"): avatar_result: Image.Image = Animals.add_border( animal_avatar, 3, False) avatar_result.save(animal_avatar_file, "png") animal_avatar_file.seek(0) if self.getRole() in animal.roles: # member is not an animal and has been before try: await animal.remove_roles(self.getRole()) await self.getChannel().send( self.text.get("result", "yes_no", mention=animal.mention), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) except Exception as e: await self.console.error(message, "Could not remove animal", e) else: # member is not an animal and has not been before await self.getChannel().send( self.text.get("result", "no_no", mention=animal.mention), file=discord.File(fp=animal_avatar_file, filename="animal.png"), ) break else: return # Edit original message result = [0, 0] for r in message.reactions: if r.emoji == "☑️": result[0] = r.count - 1 elif r.emoji == "❎": result[1] = r.count - 1 await message.edit( embed=None, content=self.text.get("edit", nickname=self.sanitise(animal.display_name), yes=result[0], no=result[1]), ) try: await message.unpin() except Exception as e: await self.console.error(message, "Could not unpin Animal vote embed", e) ## ## Logic ## async def check(self, member: discord.Member, source: str): """Create vote embed""" embed = self.embed( title=self.text.get("title"), description=f"{self.sanitise(str(member))} | {member.id}", ) embed.add_field( name=self.text.get("source", source), value=self.text.get("required", limit=self.config.get("limit")), inline=False, ) embed.set_image(url=member.avatar_url) message = await self.getChannel().send(embed=embed) await message.add_reaction("☑️") await message.add_reaction("❎") try: await message.pin() except Exception as e: await self.console.warning(member, "Could not pin Animal check embed.", e) await asyncio.sleep(0.5) messages = await message.channel.history(limit=5, after=message).flatten() for m in messages: if m.type == discord.MessageType.pins_add: await utils.delete(m) break @staticmethod def add_border(image: Image.Image, border: int, animal: bool) -> Image.Image: """Add border to created image. image: The avatar. border: width of the border. animal: whether the avatar is an animal or not. """ image_size = 160 frame_color = (22, 229, 0, 1) if animal else (221, 56, 31, 1) frame = Image.new("RGBA", (image_size + border * 2, image_size + border * 2), frame_color) frame = image_utils.round_image(frame) avatar = image_utils.round_image(image.resize( (image_size, image_size))) frame.paste(avatar, (border, border), avatar) return frame