Пример #1
0
    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")
Пример #2
0
 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()
Пример #3
0
    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)
Пример #4
0
    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.")
Пример #5
0
    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:")
Пример #6
0
    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")
Пример #7
0
    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."
            )
Пример #8
0
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()
Пример #9
0
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)