async def channel_ignore(self, ctx: Context, channel: TextChannel, mode: ChannelIgnoreMode.get = None): ignored_channel = (db_session.query(IgnoredChannel).filter( IgnoredChannel.channel == channel.id).first()) if mode == ChannelIgnoreMode.Ignore: if ignored_channel is None: # Create a new entry user = get_database_user(ctx.author) new_ignored_channel = IgnoredChannel( channel=channel.id, user_id=user.id, ) db_session.add(new_ignored_channel) try: db_session.commit() await ctx.send( f"Added {channel.mention} to the ignored list.") except SQLAlchemyError as e: db_session.rollback() logging.exception(e) await ctx.send( "Something went wrong. No change has occurred.") else: # Entry already present await ctx.send(f"{channel.mention} is already ignored!") elif mode == ChannelIgnoreMode.Watch: if ignored_channel is not None: # Remove the entry db_session.query(IgnoredChannel).filter( IgnoredChannel.channel == channel.id).delete() try: db_session.commit() await ctx.send( f"{channel.mention} is no longer being ignored.") except SQLAlchemyError as e: db_session.rollback() logging.exception(e) await ctx.send( "Something went wrong. No change has occurred.") else: # The entry is not present await ctx.send( f"{channel.mention} is not currently being ignored.") else: # Report status if ignored_channel is not None: await ctx.send(f"{channel.mention} is currently being ignored." ) else: await ctx.send( f"{channel.mention} is not currently being ignored")
async def on_member_join(self, member: Member): """Add the user to our database if they've never joined before""" user = get_database_user(member) if not user: user = User(user_uid=member.id, username=str(member)) db_session.add(user) else: user.last_seen = datetime.utcnow() try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: logging.exception(e) db_session.rollback()
async def on_message(self, message: Message): # If the message is by a bot that's not irc then ignore it if message.author.bot and not user_is_irc_bot(message): return user = get_database_user(message.author) if not user: user = User(user_uid=message.author.id, username=str(message.author)) db_session.add(user) else: user.last_seen = message.created_at # Commit the session so the user is available now try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) # Something very wrong, but not way to reliably recover so abort return # Only log messages that were in a public channel if isinstance(message.channel, GuildChannel): # Log the message to the database logged_message = LoggedMessage( message_uid=message.id, message_content=message.clean_content, author=user.id, created_at=message.created_at, channel_name=message.channel.name, ) db_session.add(logged_message) try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) return # KARMA # Get all specified command prefixes for the bot command_prefixes = self.bot.command_prefix(self.bot, message) # Only process karma if the message was not a command (ie did not start with a command prefix) if not any( message.content.startswith(prefix) for prefix in command_prefixes ): reply = process_karma( message, logged_message.id, db_session, CONFIG.KARMA_TIMEOUT ) if reply: await message.channel.send(reply)
async def channel_karma(self, ctx: Context, channel: TextChannel, mode: MiniKarmaMode.get = None): # TODO: avoid writing duplicate code with above if possible? karma_channel = (db_session.query(MiniKarmaChannel).filter( MiniKarmaChannel.channel == channel.id).first()) if mode == MiniKarmaMode.Mini: if karma_channel is None: user = get_database_user(ctx.author) new_karma_channel = MiniKarmaChannel( channel=channel.id, user_id=user.id, ) db_session.add(new_karma_channel) try: db_session.commit() await ctx.send( f"Added {channel.mention} to the mini-karma channels") except SQLAlchemyError as e: db_session.rollback() logging.exception(e) await ctx.send( "Something went wrong. No change has occurred.") else: await ctx.send( f"{channel.mention} is already on mini-karma mode!") elif mode == MiniKarmaMode.Normal: if karma_channel is not None: db_session.query(MiniKarmaChannel).filter( MiniKarmaChannel.channel == channel.id).delete() try: db_session.commit() await ctx.send( f"{channel.mention} is now on normal karma mode") except SQLAlchemyError as e: db_session.rollback() logging.exception(e) await ctx.send( "Something went wrong. No change has occurred") else: await ctx.send( f"{channel.mention} is already on normal karma mode!") else: if karma_channel is None: await ctx.send(f"{channel.mention} is on normal karma mode.") else: await ctx.send(f"{channel.mention} is on mini-karma mode.")
async def add(self, ctx: Context, item: str): author_id = get_database_user(ctx.author).id if (not db_session.query(BlockedKarma).filter( BlockedKarma.topic == item.casefold()).all()): blacklist = BlockedKarma(topic=item.casefold(), user_id=author_id) db_session.add(blacklist) try: db_session.commit() await ctx.send(f"Added {item} to the karma blacklist. :pencil:" ) except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) await ctx.send( f"Something went wrong adding {item} to the karma blacklist. No change has occurred" ) else: await ctx.send( f"{item} is already in the karma blacklist. :page_with_curl:")
async def add(self, ctx: Context, trigger_time: DateTimeConverter, *, reminder_content: str): now = datetime.now() if not trigger_time: await ctx.send("Incorrect time format, please see help text.") elif trigger_time < now: await ctx.send("That time is in the past.") else: # HURRAY the time is valid and not in the past, add the reminder display_name = get_name_string(ctx.message) # set the id to a random value if the author was the bridge bot, since we wont be using it anyways # if ctx.message.clean_content.startswith("**<"): <---- FOR TESTING if user_is_irc_bot(ctx): author_id = 1 irc_n = display_name else: author_id = get_database_user(ctx.author).id irc_n = None trig_at = trigger_time trig = False playback_ch_id = ctx.message.channel.id new_reminder = Reminder( user_id=author_id, reminder_content=reminder_content, trigger_at=trig_at, triggered=trig, playback_channel_id=playback_ch_id, irc_name=irc_n, ) db_session.add(new_reminder) try: db_session.commit() await ctx.send( f"Thanks {display_name}, I have saved your reminder (but please note that my granularity is set at {precisedelta(CONFIG.REMINDER_SEARCH_INTERVAL, minimum_unit='seconds')})." ) except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) await ctx.send(f"Something went wrong")
async def verify(self, ctx: Context, uni_number: str): # Check that the university number provided is actually formatted correctly uni_id_regex = re.compile(r"[0-9]{7}") if not re.match(uni_id_regex, uni_number): raise VerifyError( message="'{id}' is not a valid university number.".format( id=uni_number)) # Get the discord data from our servers headers = { "Authorization": "Token {token}".format(token=CONFIG.UWCS_API_TOKEN) } api_request = requests.get( "https://uwcs.co.uk/api/user/{uni_id}/".format(uni_id=uni_number), headers=headers, ) # If the request goes okay if api_request.status_code == 200: api_username = api_request.json()["discord_user"] if not api_username: # Tell the user they haven't set their discord tag on the website raise VerifyError( message= "Your Discord tag has not been set on the UWCS website - it can be set under your account " "settings: https://uwcs.co.uk/accounts/profile/update/") else: # This *shouldn't* happen but in the small case it may, just get the user to try again. Yay, async # systems! user = get_database_user(ctx.author) if not user: raise VerifyError( message= "We've hit a snag verifying your account - please try again in a few minutes!" ) # Check if the user has already verified if user.uni_id == uni_number: raise VerifyError( message= "You have already verified this university number.") # Check they are who they say they are if not api_username == str(ctx.message.author): raise VerifyError( message= "The user you're trying to verify doesn't match the tag associated with your university " "ID - please make sure you've set your tag correctly and try again." ) # Get all the objects necessary to apply the roles compsoc_guild = [ guild for guild in ctx.bot.guilds if guild.id == CONFIG.UWCS_DISCORD_ID ][0] compsoc_member = compsoc_guild.get_member( ctx.message.author.id) if not compsoc_member: raise VerifyError( message= "It seems like you're not a member of the UWCS Discord yet. You can join us here: " "https://discord.gg/uwcs") try: compsoc_role = [ role for role in compsoc_guild.roles if role.id == CONFIG.UWCS_MEMBER_ROLE_ID ][0] except IndexError: raise VerifyError( message= "I can't find the role to give you on the UWCS Discord. Let one of the exec or admins " "know so they can fix this problem!") # Give them the role and let them know await compsoc_member.add_roles( compsoc_role, reason="User verified with the university number of {uni_id}" .format(uni_id=uni_number), ) user.uni_id = uni_number user.verified_at = datetime.utcnow() try: db_session.commit() await ctx.send( "You're all verified and ready to go! Welcome to the UWCS Discord." ) except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) await ctx.send( "Could not verify you due to an internal error.") else: raise VerifyError( message= "That university number appears to be inactive or not exist - if you have just purchased " "membership please give the system 5 minutes to create an account. If you're not a member of the " "society you can purchase membership through the University of Warwick Student's union." )
def process_karma(message: Message, message_id: int, db_session: Session, timeout: int): reply = "" # Parse the message for karma modifications karma_items = parse_message_content(message.content) transactions = make_transactions(karma_items, message) transactions = filter_transactions(transactions) transactions = apply_blacklist(transactions, db_session) # If no karma'd items, just return if not transactions: return reply # TODO: Protect from byte-limit length chars # Get karma-ing user user = get_database_user(message.author) # Get whether the channel is on mini karma or not channel = (db_session.query(MiniKarmaChannel).filter( MiniKarmaChannel.channel == message.channel.id).one_or_none()) if channel is None: karma_mode = MiniKarmaMode.Normal else: karma_mode = MiniKarmaMode.Mini def own_karma_error(topic): if karma_mode == MiniKarmaMode.Normal: return f' • Could not change "{topic}" because you cannot change your own karma! :angry:' else: return f'could not change "**{topic}**" (own name)' def internal_error(topic): if karma_mode == MiniKarmaMode.Normal: return f' • Could not create "{topic}" due to an internal error.' else: return f'could not change "**{topic}**" (internal error)' def cooldown_error(topic, td): # Tell the user that the item is on cooldown if td.seconds < 60: seconds_plural = f"second{'s' if td.seconds != 1 else ''}" duration = f"{td.seconds} {seconds_plural}" else: mins = td.seconds // 60 mins_plural = f"minute{'s' if mins != 1 else ''}" duration = f"{mins} {mins_plural}" if karma_mode == MiniKarmaMode.Normal: return f' • Could not change "{topic}" since it is still on cooldown (last altered {duration} ago).\n' else: return ( f'could not change "**{topic}**" (cooldown, last edit {duration} ago)' ) def success_item(tr: KarmaTransaction): # Give some sass if someone is trying to downvote the bot if (tr.karma_item.topic.casefold() == "apollo" and tr.karma_item.operation.value < 0): apollo_response = ":wink:" else: apollo_response = "" op = str(tr.karma_item.operation) # Build the karma item string if tr.karma_item.reason: if karma_mode == MiniKarmaMode.Normal: if tr.self_karma: return f" • **{truncated_name}** (new score is {karma_change.score}) and your reason has been recorded. *Fool!* that's less karma to you. :smiling_imp:" else: return f" • **{truncated_name}** (new score is {karma_change.score}) and your reason has been recorded. {apollo_response}" else: return f"**{truncated_name}**{op} (now {karma_change.score}, reason recorded)" else: if karma_mode == MiniKarmaMode.Normal: if tr.self_karma: return f" • **{truncated_name}** (new score is {karma_change.score}). *Fool!* that's less karma to you. :smiling_imp:" else: return f" • **{truncated_name}** (new score is {karma_change.score}). {apollo_response}" else: return f"**{truncated_name}**{op} (now {karma_change.score})" # Start preparing the reply string if len(transactions) > 1: transaction_plural = "s" else: transaction_plural = "" items = [] errors = [] # Iterate over the transactions to write them to the database for transaction in transactions: # Truncate the topic safely so we 2000 char karmas can be used truncated_name = ((transaction.karma_item.topic[300:] + ".. (truncated to 300 chars)") if len(transaction.karma_item.topic) > 300 else transaction.karma_item.topic) # Catch any self-karma transactions early if transaction.self_karma and transaction.karma_item.operation.value > -1: errors.append(own_karma_error(truncated_name)) continue def topic_transformations(): def query(t): return db_session.query(Karma).filter( Karma.name.ilike(t)).one_or_none() topic = transaction.karma_item.topic.casefold() yield query(topic) yield query(topic.replace(" ", "_")) yield query(topic.replace("_", " ")) topic = unicodedata.normalize(CONFIG.UNICODE_NORMALISATION_FORM, topic) yield query(topic) yield query(topic.replace(" ", "_")) yield query(topic.replace("_", " ")) topic = "".join(c for c in topic if not unicodedata.combining(c)) yield query(topic) yield query(topic.replace(" ", "_")) yield query(topic.replace("_", " ")) # Get the karma item from the database if it exists karma_item = next(filter_out_none(topic_transformations()), None) # Update or create the karma item if not karma_item: karma_item = Karma(name=transaction.karma_item.topic) db_session.add(karma_item) try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) errors.append(internal_error(truncated_name)) continue # Get the last change (or none if there was none) last_change = (db_session.query(KarmaChange).filter( KarmaChange.karma_id == karma_item.id).order_by( desc(KarmaChange.created_at)).first()) if not last_change: # If the bot is being downvoted then the karma can only go up if transaction.karma_item.topic.casefold() == "apollo": new_score = abs(transaction.karma_item.operation.value) else: new_score = transaction.karma_item.operation.value karma_change = KarmaChange( karma_id=karma_item.id, user_id=user.id, message_id=message_id, reason=transaction.karma_item.reason, change=new_score, score=new_score, created_at=datetime.utcnow(), ) db_session.add(karma_change) try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) errors.append(internal_error(truncated_name)) continue else: time_delta = datetime.utcnow() - last_change.created_at if is_in_cooldown(last_change, timeout): errors.append(cooldown_error(truncated_name, time_delta)) continue # If the bot is being downvoted then the karma can only go up if transaction.karma_item.topic.casefold() == "apollo": new_score = last_change.score + abs( transaction.karma_item.operation.value) else: new_score = last_change.score + transaction.karma_item.operation.value karma_change = KarmaChange( karma_id=karma_item.id, user_id=user.id, message_id=message_id, reason=transaction.karma_item.reason, score=new_score, change=(new_score - last_change.score), created_at=datetime.utcnow(), ) db_session.add(karma_change) try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) errors.append(internal_error(truncated_name)) karma_change = KarmaChange( karma_id=karma_item.id, user_id=user.id, message_id=message_id, reason=transaction.karma_item.reason, score=new_score, change=(new_score - last_change.score), created_at=datetime.utcnow(), ) db_session.add(karma_change) try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: db_session.rollback() logging.exception(e) errors.append(internal_error(truncated_name)) continue # Update karma counts if transaction.karma_item.operation.value == 0: karma_item.neutrals = karma_item.neutrals + 1 elif transaction.karma_item.operation.value == 1: karma_item.pluses = karma_item.pluses + 1 elif transaction.karma_item.operation.value == -1: # Make sure the changed operation is updated if transaction.karma_item.topic.casefold() == "apollo": karma_item.pluses = karma_item.pluses + 1 else: karma_item.minuses = karma_item.minuses + 1 items.append(success_item(transaction)) # Get the name, either from discord or irc author_display = get_name_string(message) # Construct the reply string in totality # If you have error(s) and no items processed successfully if karma_mode == MiniKarmaMode.Normal: item_str = "\n".join(items) error_str = "\n".join(errors) if not item_str and error_str: reply = f"Sorry {author_display}, I couldn't karma the requested item{transaction_plural} because of the following problem{transaction_plural}:\n\n{error_str}" # If you have items processed successfully but some errors too elif item_str and error_str: reply = f"Thanks {author_display}, I have made changes to the following item(s) karma:\n\n{item_str}\n\nThere were some issues with the following item(s), too:\n\n{error_str}" # If all items were processed successfully else: reply = f"Thanks {author_display}, I have made changes to the following karma item{transaction_plural}:\n\n{item_str}" else: item_str = " ".join(items) error_str = " ".join(errors) reply = " ".join(filter(None, ["Changes:", item_str, error_str])) # Commit any changes (in case of any DB inconsistencies) try: db_session.commit() except (ScalarListException, SQLAlchemyError) as e: logging.exception(e) db_session.rollback() return reply.rstrip()
def ctx_to_mention(ctx): """Convert requester name to Mention""" if user_is_irc_bot(ctx): return Mention.string_mention(get_name_string(ctx)) else: return Mention.id_mention(get_database_user(ctx.author).id)