<commit_before>import re
from asyncio import sleep
from contextlib import closing
from datetime import datetime, timedelta
from functools import partial, wraps
from logging import getLogger

from discord import Forbidden, HTTPException, Member, Message, Permissions, TextChannel
from discord.abc import PrivateChannel
from discord.ext.commands import (
    Cog, Command, bot_has_permissions, group, guild_only, has_permissions
)
from discord.utils import get

from ..context import Context
from ..db import NewbieChannel, NewbieGuild, NewbieUser
from ..errors import PromptTimeout
from ..utils import clean_prefix, prompt

logger = getLogger(__name__)

# Matches either a channel mention of the form "<#id>" or a raw ID.
# The actual ID can be extracted from the 'id' group of the match object
channel_re = re.compile(r'((<#)|^)(?P<id>\d+)(?(2)>|(\s|$))')


def newbie_enabled(func):
    """Decorator to check if newbie roling is enabled before running the command."""

    if isinstance(func, Command):
        cmd = func.callback
    else:
        cmd = func

    @wraps(cmd)
    async def wrapper(*args, **kwargs):
        # No try-catch necessary, context is always passed since rewrite
        ctx = next(i for i in args if isinstance(i, Context))

        if not (ctx.guild and ctx.session.query(NewbieGuild).get(ctx.guild.id)):
            await ctx.send('Newbie roling is not enabled on this server. '
                           'Please enable it before using these commands.')
            return

        await cmd(*args, **kwargs)

    if isinstance(func, Command):
        func.callback = wrapper
        return func
    else:
        return wrapper


class Newbies(Cog):
    def __init__(self, bot, loop, scoped_session, sessionmaker, check_period=60):
        self.bot = bot
        self._check_period = check_period
        self._session = scoped_session
        self._sessionmaker = sessionmaker
        loop.create_task(self.check_timeouts())

    async def check_timeouts(self):
        await self.bot.wait_until_ready()
        while True:
            with closing(self._sessionmaker()) as session:
                # Get users who have passed the timeout
                # !IMPORTANT! Filter inequation must not be changed
                # Changing it will break SQLite support (and possibly other DBMSs as well)
                q = session.query(NewbieUser)\
                    .join(NewbieUser.guild)\
                    .filter(datetime.utcnow() > NewbieGuild.timeout + NewbieUser.joined_at)

                for db_user in q:
                    guild = self.bot.get_guild(db_user.guild_id)
                    if not guild:
                        continue

                    member = guild.get_member(db_user.user_id)
                    if not member:
                        session.delete(db_user)
                        continue

                    try:
                        await member.kick(reason='Verification timed out.')
                        session.delete(db_user)
                        logger.info(f'Kicked overdue user {member} from guild {guild}.')
                    except Forbidden:
                        logger.exception(
                            f'Lacking permissions to kick user {member} from guild {guild}.'
                        )
                    except HTTPException as e:
                        logger.exception(
                            f'Failed to kick user {member} from guild {guild} '
                            f'due to HTTP error {e.response.status}.'
                        )

                session.commit()

            await sleep(self._check_period)

    async def add_member(self, db_guild: NewbieGuild, member: Member):
        if self._session.query(NewbieUser).get((member.id, db_guild.guild_id)):
            return  # Exit if user already in DB

        message_content = db_guild.welcome_message

        message_content += '\n'
        message_content += f'Please reply with the following message to be granted access to ' \
            f'"{member.guild}".\n'
        message_content += f'```{db_guild.response_message}```'

        try:
            message = await member.send(message_content)
        except Forbidden:
            logger.exception(
                f'Cannot message user {member} and thus cannot prompt for verification.'
            )
            return
        except HTTPException as e:
            logger.exception(
                f'Failed sending message to user {member} due to HTTP error {e.response.status}.'
            )
            return
        else:
            # Use utcnow() instead of join time to treat members who got the message too late fairly
            db_user = NewbieUser(guild_id=member.guild.id,
                                 user_id=member.id,
                                 message_id=message.id,
                                 joined_at=datetime.utcnow())
            self._session.add(db_user)
            self._session.commit()
            logger.info('Added new user {0} to database for guild {0.guild}.'.format(member))

            try:
                # Necessary in compliance with Discord's latest ToS changes ¯\_(ツ)_/¯
                await member.send(
                    f'Please note that by staying on "{member.guild}", '
                    'you agree that this bot stores your user ID for identification purposes.\n'
                    'It shall be deleted once you confirm the above message or leave the server.'
                )
            except HTTPException:
                # First message went through, no need to further handle this, should it ever occur
                pass

    @Cog.listener()
    async def on_ready(self):
        for db_guild in self._session.query(NewbieGuild):
            guild = self.bot.get_guild(db_guild.guild_id)
            if not guild:
                continue

            member_role = guild.get_role(db_guild.role_id)
            if not member_role:
                continue

            to_add = (member for member in guild.members if member_role not in member.roles)
            for member in to_add:
                await self.add_member(db_guild, member)

    @Cog.listener()
    async def on_member_join(self, member: Member):
        db_guild = self._session.query(NewbieGuild).get(member.guild.id)

        if db_guild is None:
            return

        # Bots don't need confirmation, since they were manually added by a mod/admin
        # Not like they could confirm themselves anyways
        if member.bot:
            role = member.guild.get_role(db_guild.role_id)
            if not role:
                return

            await member.add_roles(role)
            return

        await self.add_member(db_guild, member)

    @Cog.listener()
    async def on_member_remove(self, member: Member):
        # Necessary in compliance with Discord's latest ToS changes ¯\_(ツ)_/¯
        # Use query instead of object deletion to prevent redundant SELECT query
        self._session.query(NewbieUser)\
            .filter(NewbieUser.user_id == member.id, NewbieUser.guild_id == member.guild.id)\<commit_msg>fix(newbie): exempt bots from confirmation<commit_after>import re
from asyncio import sleep
from contextlib import closing
from datetime import datetime, timedelta
from functools import partial, wraps
from logging import getLogger

from discord import Forbidden, HTTPException, Member, Message, Permissions, TextChannel
from discord.abc import PrivateChannel
from discord.ext.commands import (
    Cog, Command, bot_has_permissions, group, guild_only, has_permissions
)
from discord.utils import get

from ..context import Context
from ..db import NewbieChannel, NewbieGuild, NewbieUser
from ..errors import PromptTimeout
from ..utils import clean_prefix, prompt

logger = getLogger(__name__)

# Matches either a channel mention of the form "<#id>" or a raw ID.
# The actual ID can be extracted from the 'id' group of the match object
channel_re = re.compile(r'((<#)|^)(?P<id>\d+)(?(2)>|(\s|$))')


def newbie_enabled(func):
    """Decorator to check if newbie roling is enabled before running the command."""

    if isinstance(func, Command):
        cmd = func.callback
    else:
        cmd = func

    @wraps(cmd)
    async def wrapper(*args, **kwargs):
        # No try-catch necessary, context is always passed since rewrite
        ctx = next(i for i in args if isinstance(i, Context))

        if not (ctx.guild and ctx.session.query(NewbieGuild).get(ctx.guild.id)):
            await ctx.send('Newbie roling is not enabled on this server. '
                           'Please enable it before using these commands.')
            return

        await cmd(*args, **kwargs)

    if isinstance(func, Command):
        func.callback = wrapper
        return func
    else:
        return wrapper


class Newbies(Cog):
    def __init__(self, bot, loop, scoped_session, sessionmaker, check_period=60):
        self.bot = bot
        self._check_period = check_period
        self._session = scoped_session
        self._sessionmaker = sessionmaker
        loop.create_task(self.check_timeouts())

    async def check_timeouts(self):
        await self.bot.wait_until_ready()
        while True:
            with closing(self._sessionmaker()) as session:
                # Get users who have passed the timeout
                # !IMPORTANT! Filter inequation must not be changed
                # Changing it will break SQLite support (and possibly other DBMSs as well)
                q = session.query(NewbieUser)\
                    .join(NewbieUser.guild)\
                    .filter(datetime.utcnow() > NewbieGuild.timeout + NewbieUser.joined_at)

                for db_user in q:
                    guild = self.bot.get_guild(db_user.guild_id)
                    if not guild:
                        continue

                    member = guild.get_member(db_user.user_id)
                    if not member:
                        session.delete(db_user)
                        continue

                    try:
                        await member.kick(reason='Verification timed out.')
                        session.delete(db_user)
                        logger.info(f'Kicked overdue user {member} from guild {guild}.')
                    except Forbidden:
                        logger.exception(
                            f'Lacking permissions to kick user {member} from guild {guild}.'
                        )
                    except HTTPException as e:
                        logger.exception(
                            f'Failed to kick user {member} from guild {guild} '
                            f'due to HTTP error {e.response.status}.'
                        )

                session.commit()

            await sleep(self._check_period)

    async def add_member(self, db_guild: NewbieGuild, member: Member):
        # Bots are exempt from confirmation
        # This should also avoid the bot trying and failing to send messages to itself
        if member.bot and (member_role := member.guild.get_role(db_guild.role_id)):
            await member.add_roles(member_role)
            return

        if self._session.query(NewbieUser).get((member.id, db_guild.guild_id)):
            return  # Exit if user already in DB

        message_content = db_guild.welcome_message

        message_content += '\n'
        message_content += f'Please reply with the following message to be granted access to ' \
            f'"{member.guild}".\n'
        message_content += f'```{db_guild.response_message}```'

        try:
            message = await member.send(message_content)
        except Forbidden:
            logger.exception(
                f'Cannot message user {member} and thus cannot prompt for verification.'
            )
            return
        except HTTPException as e:
            logger.exception(
                f'Failed sending message to user {member} due to HTTP error {e.response.status}.'
            )
            return
        else:
            # Use utcnow() instead of join time to treat members who got the message too late fairly
            db_user = NewbieUser(guild_id=member.guild.id,
                                 user_id=member.id,
                                 message_id=message.id,
                                 joined_at=datetime.utcnow())
            self._session.add(db_user)
            self._session.commit()
            logger.info('Added new user {0} to database for guild {0.guild}.'.format(member))

            try:
                # Necessary in compliance with Discord's latest ToS changes ¯\_(ツ)_/¯
                await member.send(
                    f'Please note that by staying on "{member.guild}", '
                    'you agree that this bot stores your user ID for identification purposes.\n'
                    'It shall be deleted once you confirm the above message or leave the server.'
                )
            except HTTPException:
                # First message went through, no need to further handle this, should it ever occur
                pass

    @Cog.listener()
    async def on_ready(self):
        for db_guild in self._session.query(NewbieGuild):
            guild = self.bot.get_guild(db_guild.guild_id)
            if not guild:
                continue

            member_role = guild.get_role(db_guild.role_id)
            if not member_role:
                continue

            to_add = (member for member in guild.members if member_role not in member.roles)
            for member in to_add:
                await self.add_member(db_guild, member)

    @Cog.listener()
    async def on_member_join(self, member: Member):
        db_guild = self._session.query(NewbieGuild).get(member.guild.id)

        if db_guild is None:
            return

        await self.add_member(db_guild, member)

    @Cog.listener()
    async def on_member_remove(self, member: Member):
        # Necessary in compliance with Discord's latest ToS changes ¯\_(ツ)_/¯
        # Use query instead of object deletion to prevent redundant SELECT query
        self._session.query(NewbieUser)\
            .filter(NewbieUser.user_id == member.id, NewbieUser.guild_id == member.guild.id)\
{'fraction_non_alphanumeric': 0.07840662038011081, 'fraction_numerical': 0.000701311452416018, 'mean_word_length': 1.8657556270096463, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '93482', 'n_tokens_mistral': 3844, 'n_tokens_neox': 3679, 'n_words': 1172}