def setup_module(): global SESSION, BASE_SERVER, BASE_USER SESSION = manager.get_session(botconfig, db_config="TestDatabase") BASE_USER = models.User(discord_id=randint(10, 10000)) SESSION.add(BASE_USER) BASE_SERVER = models.Server(discord_id=randint(10, 10000)) SESSION.add(BASE_SERVER) SESSION.commit()
def test_positive_create_user(): """Create a user and verify it exists in the database""" discord_id = randint(10, 10000) new_user = models.User(discord_id=discord_id) SESSION.add(new_user) SESSION.commit() user = SESSION.query( models.User).filter(models.User.discord_id == discord_id).one() assert user.discord_id == discord_id assert user.created
def test_positive_create_alias(): """Create a user and alias, assigning the latter to the former""" discord_id = randint(10, 10000) new_user = models.User(discord_id=discord_id) SESSION.add(new_user) alias_text = "my cool alias#0000" new_alias = models.Alias(name=alias_text, user=new_user) SESSION.add(new_alias) SESSION.commit() assert (SESSION.query(models.Alias.name).filter( models.Alias.user == new_user).one()[0] == alias_text)
async def db_get_user(self, session, discord_id): try: # Try and get record from database db_user = (session.query(models.User).filter( models.User.discord_id == discord_id).first()) # If no DB record for the user then create one if not db_user: db_user = models.User(discord_id=discord_id) session.add(db_user) session.commit() self.bot.log.debug(f"Added new user to DB: {discord_id}") return db_user except Exception as err: self.bot.log.exception( f"Error getting / adding user to database: '{discord_id}'. {sys.exc_info()[0].__name__}: {err}" )
def test_positive_action_warn(): """This test also covers Note, Mute, and Ban""" discord_id, warn_text = randint(10, 10000), "this is a test warning" logged_action = models.Action(mod=BASE_USER, server=BASE_SERVER) new_warn = models.Warn( text=warn_text, user=models.User(discord_id=discord_id), server=BASE_SERVER, action=logged_action, ) SESSION.add(new_warn) SESSION.commit() user = SESSION.query( models.User).filter(models.User.discord_id == discord_id).one() assert new_warn in user.warn warn = SESSION.query(models.Warn).filter(models.Warn.user == user).first() assert warn.text == warn_text assert warn.action == logged_action
async def handle_outgoing_chan_from_mod(self, message): user = None # Check if we have a user for the mod mail channel the message is being sent in user_id = self.redis.get( f"user_id:bid:{self.bot.user.id}:mmcid:{message.channel.id}") # If no user ID, such as message sent in a non user mod mail channel, just stop processing if not user_id: return # Try to get the user from the bots caching else: user = self.bot.get_user(int(user_id)) # Try to get from an API call if not user: user = await self.bot.fetch_user(int(user_id)) # If no user, let mods know, stop processing if not user: self.bot.log.exception( f"Unable to find a user from the User ID: {user_id}. This could be due to bad Redis cache data.\n\n**Redis Data:** 'user_id:bid:{self.bot.user.id}:mmcid:{message.channel.id}'" ) return await message.channel.send( f"Unable to find a user from the User ID: {user_id}. Please validate it's the correct User ID for the user. This has already been reported to my developers." ) # If the user is blacklisted, exit out (but let's tell the mods first) if await self.bot.helpers.check_if_blacklisted(user.id, self.modmail_server_id): self.bot.log.debug( f"User {message.author} ({message.author.id}) Blacklisted, unable to use Mod Mail" ) return await message.channel.send( "\N{CROSS MARK} Sorry, unable to send to that user as they are **blacklisted.**" ) # Now that we have the channel, we need to process it session = self.bot.helpers.get_db_session() try: msg_body = f"{message.content[:1900]}\n\n-{message.author}" # Step 1: Send the mods message to the user if message.attachments: all_links = [] for file in message.attachments: new_file = discord.File(io.BytesIO(await file.read()), filename=file.filename) all_links.append(new_file) # Once we have all files, send to the user msg_to_user = await user.send(msg_body, files=all_links) else: # If no attachments send regular message msg_to_user = await user.send(msg_body) # Step 2: Send the message to the mod server embed = discord.Embed(color=0x551A8B, timestamp=datetime.utcnow(), description=msg_body) embed.set_author( name=f"Member: {message.author} ({message.author.id})", icon_url=message.author.avatar_url, ) # Create list of links file_links = [] if msg_to_user.attachments: all_links = [] counter = 0 for file in msg_to_user.attachments: counter += 1 word = num2words(counter) link = f"Link [{word}]({file.proxy_url})" all_links.append(link) file_links.append(file.proxy_url) attachments_text = ", ".join(all_links) embed.add_field(name="Attachments", value=f"{attachments_text}", inline=False) embed.add_field(name="Bot/User Channel ID", value=f"{user.dm_channel.id}", inline=False) embed.add_field(name="Bot/User Message ID", value=f"{msg_to_user.id}", inline=False) # Set the footer embed.set_footer(text=f"Outgoing Mod Mail") # Send the message in the channel await message.channel.send(embed=embed) # If message successfully sends, delete the calling message await message.delete() # Step 3: Move to the In Progress category, only if it was in unanswered if message.channel.category_id == self.modmail_unanswered_cat.id: try: await message.channel.edit( category=self.modmail_in_progress_cat) except discord.errors.HTTPException as err: if err.code == 50035: self.bot.log.warning( f"Unable to move channel. Error: {err}") # Update category counts # 2020/06/19 - Disabling Category Count Update due to B9940-121 # await self.update_cat_count( # self.modmail_unanswered_cat, unanswered=True # ) # await self.update_cat_count( # self.modmail_in_progress_cat, unanswered=False # ) # Step 4: Log to the database # Check if there is a user in the database already db_user = (session.query(models.User).filter( models.User.discord_id == message.author.id).first()) # If no DB record for the user then create one if not db_user: db_user = models.User(discord_id=message.author.id) session.add(db_user) # Get the mod mail guild db_mm_guild = (session.query(models.Server).filter( models.Server.discord_id == self.mm_guild.id).first()) # Get the main guild db_main_guild = (session.query(models.Server).filter( models.Server.discord_id == self.main_guild.id).first()) # Create the data to inject data = { "mm_channel_id": message.channel.id, "user_channel_id": user.dm_channel.id, "message_id": msg_to_user.id, "message": msg_body, "from_mod": True, "file_links": file_links, } new_message = models.ModMailMessage( primary_server=db_main_guild, mm_server=db_mm_guild, user=db_user, **data, ) session.add(new_message) session.commit() except DBAPIError as err: self.bot.log.exception( f"Error processing database query for outgoing mod mail. {sys.exc_info()[0].__name__}: {err}" ) session.rollback() except discord.Forbidden: await message.channel.send( f"Unable to send messages to this user. They may have blocked the bot or don't share any servers with the bot anymore." ) except Exception as err: self.bot.log.exception( f"Unknown exception processing outgoing mod mail. {sys.exc_info()[0].__name__}: {err}" ) await message.channel.send( f"There was an error processing the outgoing mod mail. Please wait a few minutes and try again. If you are still having issues, please contact the bot developers." f"\n\nThis error has already been reported to my developers. Sorry for the inconvenience." ) finally: session.close()
async def handle_incoming_dm_from_user(self, ctx, message): self.bot.log.debug( f"ModMail: New Message From: {message.author} ({message.author.id})" ) # If the user is blacklisted, exit out if await self.bot.helpers.check_if_blacklisted(message.author.id, self.modmail_server_id): self.bot.log.debug( f"ModMail: User {message.author} ({message.author.id}) Blacklisted, unable to use Mod Mail" ) return # Check if user is on cooldown/rate limited # Also check if the antispam modmail quickmsg feature is enabled settings = self.bot.guild_settings.get(self.main_guild.id) if settings and settings.antispam_quickmsg_modmail: bucket = self._cd_modmail_incoming.get_bucket(message) retry_after = bucket.update_rate_limit() if retry_after: # you're rate limited helpful message here self.bot.log.debug( f"ModMail: User {message.author} ({message.author.id}) is on cooldown/rate limited, unable to use Mod Mail. Expires: {retry_after:0.2f} seconds" ) await message.channel.send( f"You are currently on cooldown. Please decrease the rate at which you send us messages or you may find yourself blacklisted.\n\nYou may send messages again in {retry_after:0.2f} seconds." ) return # you're not rate limited, continue new_channel_created = False # Check if we have a channel in the mod mail server yet mm_channel_id = self.redis.get( f"mm_chanid:bid:{self.bot.user.id}:uid:{message.author.id}") self.bot.log.debug( f"ModMail: Redis mm_channel_id: {mm_channel_id} User: {message.author} ({message.author.id})" ) # Try to get the channel mm_channel = None if mm_channel_id: mm_channel = self.bot.get_channel(int(mm_channel_id)) # If no channel, create one if not mm_channel: mm_channel = await self.make_modmail_channel(message.author) # Additional check to make sure a mod mail channel exists if not mm_channel: self.bot.log.exception( f"ModMail: We just made a mod mail channel, why are we saying there isn't one for user: ({message.author.id})" ) return # When we get an incoming message we know all 3 parts: # 1. The bot ID # 2. The mod mail channel ID # 3. The users ID # We take all this info and store it, so when we get an incoming message we know the mod mail channel # to send it to. When we have an outgoing message we can find the user ID as at any given point # we know 2 parts and need to find the 3rd. # # This sets the mod mail channel ID with a key of the bot ID and the author ID # TO DO - Could move the redis cache setting into self.make_modmail_channel so it happens upon creation self.redis.set( f"mm_chanid:bid:{self.bot.user.id}:uid:{message.author.id}", f"{mm_channel.id}", ) self.bot.log.debug( f"ModMail: Redis SET: 'mm_chanid:bid:{self.bot.user.id}:uid:{message.author.id}' TO '{mm_channel.id}'" ) # This sets the users ID with a key of the bot ID and the mod mail channel ID self.redis.set( f"user_id:bid:{self.bot.user.id}:mmcid:{mm_channel.id}", f"{message.author.id}", ) self.bot.log.debug( f"ModMail: Redis SET: 'user_id:bid:{self.bot.user.id}:mmcid:{mm_channel.id}' TO '{message.author.id}'" ) new_channel_created = True # Now that we have the channel, we need to process it session = self.bot.helpers.get_db_session() try: # Step 2: Get the users history and send it: ( embed_result_entries, footer_text, ) = await self.bot.helpers.get_action_history( session, message.author, self.main_guild) p = FieldPages( ctx, per_page=8, entries=embed_result_entries, mm_channel=mm_channel, ) p.embed.color = 0xFF8C00 p.embed.set_author( name=f"Member: {message.author} ({message.author.id})", icon_url=message.author.avatar_url, ) p.embed.set_footer(text=footer_text) # Step 3: Send the users message to the mod mail channel msg_body = message.clean_content[:2000] embed = discord.Embed(color=0x19D219, timestamp=datetime.utcnow(), description=msg_body) embed.set_author( name=f"Member: {message.author} ({message.author.id})", icon_url=message.author.avatar_url, ) file_links = [] if message.attachments: all_links = [] counter = 0 for file in message.attachments: counter += 1 word = num2words(counter) link = f"Link [{word}]({file.url})" all_links.append(link) file_links.append(file.url) attachments_text = ", ".join(all_links) embed.add_field(name="Attachments", value=f"{attachments_text}", inline=False) embed.add_field(name="Bot/User Channel ID", value=f"{message.channel.id}", inline=False) embed.add_field(name="Bot/User Message ID", value=f"{message.id}", inline=False) # Set the footer embed.set_footer(text=f"Incoming Mod Mail") # If this is a new interaction (defined by having to create a new channel in the mod mail server) if new_channel_created: # Send the history try: await p.paginate(modmail_bypass=True) except discord.errors.HTTPException: self.bot.log.error( f"Error sending History in Mod Mail for {message.author.id}" ) # Let the user know that we will use reactions to signify their message was received await message.channel.send( self.bot.constants.modmail_read_receipts) # Always send the users message to us await mm_channel.send(embed=embed) # Step 4: Let the user know we received their message await message.add_reaction("✉") # Step 5: Log to the database # Check if there is a user in the database already db_user = (session.query(models.User).filter( models.User.discord_id == message.author.id).first()) # If no DB record for the user then create one if not db_user: db_user = models.User(discord_id=message.author.id) session.add(db_user) # Get the mod mail guild db_mm_guild = (session.query(models.Server).filter( models.Server.discord_id == self.mm_guild.id).first()) # Get the main guild db_main_guild = (session.query(models.Server).filter( models.Server.discord_id == self.main_guild.id).first()) # Create the data to inject data = { "mm_channel_id": mm_channel.id, "user_channel_id": message.channel.id, "message_id": message.id, "message": msg_body, "from_mod": False, "file_links": file_links, } new_message = models.ModMailMessage( primary_server=db_main_guild, mm_server=db_mm_guild, user=db_user, **data, ) session.add(new_message) session.commit() except DBAPIError as err: self.bot.log.exception( f"Error processing database query for an incoming mod mail. {sys.exc_info()[0].__name__}: {err}" ) session.rollback() except Exception as err: self.bot.log.exception( f"Unknown exception processing incoming mod mail. {sys.exc_info()[0].__name__}: {err}" ) await message.channel.send( f"There was an error processing your mod mail. Please wait a few minutes and try again. If you are still having issues, contact a mod directly." f"\n\nThis error has already been reported to my developers. Sorry for the inconvenience." ) finally: session.close()
async def request(self, ctx, *, request_body): """Takes input as a request and reposts it in a dedicated channel and provides voting reactions to show interest. Logs to database for record keeping. Detects duplication. Requires Permission ------------------- Send Messages """ session = self.bot.helpers.get_db_session() try: self.bot.log.info( f"CMD {ctx.invoked_with} called by {ctx.message.author} ({ctx.message.author.id})" ) # Check if user is blacklisted, if so, ignore. if await self.bot.helpers.check_if_blacklisted( ctx.message.author.id, ctx.message.guild.id): self.bot.log.debug( f"User {ctx.message.author} ({ctx.message.author.id}) Blacklisted, unable to use command {ctx.command}" ) return # Get channel ID's the command is allowed in guild = ctx.message.guild settings = self.bot.guild_settings.get(guild.id) if settings.request_type == models.RequestType.suggestion: footer = f"Usage: '{ctx.prefix}suggestion [your suggestion]" else: footer = f"Usage: '{ctx.prefix}request Game Title (Release Date)'" upvote_emoji = settings.upvote_emoji or self.bot.constants.reactions[ "upvote"] downvote_emoji = settings.downvote_emoji or self.bot.constants.reactions[ "downvote"] question_emoji = settings.question_emoji or self.bot.constants.reactions[ "question"] downvotes_allowed = settings.allow_downvotes questions_allowed = settings.allow_questions request_channel = settings.request_channel request_channel_allowed = settings.request_channel_allowed if request_channel_allowed is None: return await ctx.send( f"No requests allowed channel found. Please set one on the configuration." ) if request_channel is None: return await ctx.send( f"No requests channel found. Please set one on the configuration." ) temp_request_channel_allowed_name = [] for temp_channel_id in request_channel_allowed: temp_channel = self.bot.get_channel(temp_channel_id) if temp_channel: temp_request_channel_allowed_name.append(temp_channel) request_channel_allowed_name = [ f"{channel.name}" for channel in temp_request_channel_allowed_name ] request_channel_allowed_clean = ", ".join( request_channel_allowed_name) if ctx.message.channel.id not in request_channel_allowed: # Tries to let user know in DM that cmd not allowed in that channel, if that fails send in channel. # Next tries to delete calling command to reduce spam try: await ctx.message.author.send( f"This command can only be used in the channels: {request_channel_allowed_clean} \n\n >>> {request_body[:1850]}" ) except discord.errors.Forbidden: await ctx.send( f"This command can only be used in the channels: {request_channel_allowed_clean}" ) try: await ctx.message.delete() except discord.errors.Forbidden: pass # Stop processing command if not done in right channel return # Check if request exists guild_requests = (session.query(models.Requests).join( models.Server, models.Server.id == models.Requests.server_id ).filter(models.Server.discord_id == ctx.message.guild.id).filter( models.Requests.status == models.RequestStatus.open).all()) # Check for direct duplicates if request_body[:1900].lower() in [ singleRequest.text.lower() for singleRequest in guild_requests ]: for singleRequest in guild_requests: title = getattr(singleRequest, "text") if request_body[:1900].lower() == title.lower(): dupe_link = getattr(singleRequest, "message_id") await ctx.message.delete() dupe_embed = discord.Embed( color=0x00CC00, title="Found it!", description= f"It looks like a request for this title already exists! You can view the existing request [here](https://discord.com/channels/{ctx.guild.id}/{ctx.channel.id}/{dupe_link}).\nRemember to upvote it!", timestamp=datetime.utcnow(), ).set_footer( text="This message will be removed in 15 seconds.") await (await ctx.channel.send(embed=dupe_embed )).delete(delay=15) return # Loop through existing requests for singleRequest in guild_requests: game_title = getattr(singleRequest, "text") message_link = getattr(singleRequest, "message_id") # Check for substrings in the text if (re.sub('[-!$%^&*()_+|~=`{}\[\]:\";\'<>?,.\/\s+]', '', request_body[:1900].lower()) in re.sub( '[-!$%^&*()_+|~=`{}\[\]:\";\'<>?,.\/\s+]', '', game_title.lower()) or re.sub('[-!$%^&*()_+|~=`{}\[\]:\";\'<>?,.\/\s+]', '', game_title.lower()) in re.sub('[-!$%^&*()_+|~=`{}\[\]:\";\'<>?,.\/\s+]', '', request_body[:1900].lower())): # Check function for reactions (yes / no) def check(reaction, user): return user == ctx.author and ( reaction.emoji == self.bot.get_emoji( self.bot.constants.reactions["yes"]) or reaction.emoji == self.bot.get_emoji( self.bot.constants.reactions["no"])) # Embed to display when a potential duplicate entry is found found_embed = discord.Embed( color=0xFFA500, title= "I've found an existing request quite similar to yours! Is this the title you wanted to request?", description=f">>> {game_title}", timestamp=datetime.utcnow(), ).set_footer( text= "This message will timeout in 60 seconds and your request will be removed without a response." ) msg = await ctx.channel.send(embed=found_embed) # Reactions for the user to react on yes = self.bot.get_emoji( self.bot.constants.reactions["yes"]) no = self.bot.get_emoji(self.bot.constants.reactions["no"]) # Add the reactions for emoji in (yes, no): if emoji: await msg.add_reaction(emoji) try: # Wait for the user to confirm or deny if duplicate reaction, user = await self.bot.wait_for( "reaction_add", check=check, timeout=60.0) except asyncio.TimeoutError: # Delete message on timeout await msg.delete() await ctx.message.delete() return else: # Delete message on reaction await msg.delete() # If user replies yes, link to the existing request if reaction.emoji == self.bot.get_emoji( self.bot.constants.reactions["yes"]): await ctx.message.delete() existing_embed = discord.Embed( color=0x00CC00, title="Found it!", description= f"Great! You can view the existing request [here](https://discord.com/channels/{ctx.guild.id}/{ctx.channel.id}/{message_link}).\nRemember to upvote it!", timestamp=datetime.utcnow(), ).set_footer( text= "This message will be removed in 15 seconds.") await (await ctx.channel.send(embed=existing_embed )).delete(delay=15) return # Create the embed of info embed = discord.Embed( color=0x14738E, title= f"Port Request from {ctx.message.author} ({ctx.message.author.id})", description=f">>> {request_body[:1900]}", timestamp=datetime.utcnow(), ) embed.set_footer(text=f"{footer}") channel = ctx.message.guild.get_channel(request_channel) if channel: try: msg = await channel.send(embed=embed) upvote = self.bot.get_emoji(upvote_emoji) downvote = self.bot.get_emoji( downvote_emoji) if downvotes_allowed else None question = self.bot.get_emoji( question_emoji) if questions_allowed else None # Add the reactions for emoji in (upvote, downvote, question): if emoji: await msg.add_reaction(emoji) # Now let user know it was posted - but if it's in same channel it's being posted to, no need if ctx.message.channel.id == channel.id: # Delete the request command, no feedback try: await ctx.message.delete() except discord.errors.Forbidden: pass else: await ctx.message.delete() await (await ctx.send( f"Thank you for your request, it has now been posted and is available in {channel.mention}" )).delete(delay=15) # Now let's log it to the database try: # Check if there is a user in the database already db_user = (session.query( models.User).filter(models.User.discord_id == ctx.message.author.id).first()) # If no DB record for the user then create one if not db_user: db_user = models.User( discord_id=ctx.message.author.id) session.add(db_user) # Check if there is a guild in the database already db_guild = (session.query(models.Server).filter( models.Server.discord_id == msg.guild.id).first()) if not db_guild: db_guild = await self.bot.helpers.db_add_new_guild( session, msg.guild.id) new_record = models.Requests( user=db_user, server=db_guild, message_id=msg.id, text=request_body, ) session.add(new_record) session.commit() except DBAPIError as err: self.bot.log.exception( f"Error processing database query for '{ctx.command}' command. {sys.exc_info()[0].__name__}: {err}" ) session.rollback() except Exception as err: self.bot.log.exception( f"Unknown Error logging to database for to '{ctx.command}' command via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) finally: session.close() except discord.errors.Forbidden: await ctx.send( f"Sorry, I lack permissions to be able to submit that request" ) except Exception as err: self.bot.log.exception( f"Error responding to {ctx.command} via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) try: await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) except discord.errors.Forbidden: pass except discord.HTTPException as err: self.bot.log.exception( f"Discord HTTP Error responding to {ctx.command} request via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." ) except Exception as err: self.bot.log.exception( f"Error responding to {ctx.command} via Msg ID {ctx.message.id}. {sys.exc_info()[0].__name__}: {err}" ) await ctx.send( f"Error processing {ctx.command}. Error has already been reported to my developers." )