Esempio n. 1
0
    async def announce(self,
                       context: commands.Context,
                       poll_id: int,
                       announce: str,
                       *,
                       options=""):
        message, channel, _, _, _, _, organizer = await self.get_message_env(
            poll_id, raise_if_not_found=True)

        if context.author != organizer:
            checker.has_any_mod_role(context, print_error=True)
        if not context.author.permissions_in(channel).send_messages:
            raise exceptions.ForbiddenChannel(channel)
        do_announce = utils.is_option_enabled(options, 'do-announce')
        do_pin = utils.is_option_enabled(options, 'pin')
        if do_announce or do_pin:
            checker.has_any_mod_role(context, print_error=True)

        prefixed_announce = utils.make_announce(
            context.guild, announce, do_announce and self.ANNOUNCE_ROLE_NAME)
        if do_pin and not message.pinned:
            await message.pin()
        elif not do_pin and message.pinned:
            await message.unpin()
        await message.edit(content=prefixed_announce)
        await context.send(
            f"Annonce du sondage d'identifiant `{poll_id}` remplacée par "
            f"\"`{message.clean_content}`\" : <{message.jump_url}>")
Esempio n. 2
0
File: server.py Progetto: Zedd7/ZBot
    async def parse_time_arguments(options, default_days_number=30):
        # Check arguments
        time_option = utils.get_option_value(options, 'time')
        if time_option is not None:  # Value assigned
            try:
                days_number = int(time_option)
            except ValueError:
                raise exceptions.MisformattedArgument(time_option,
                                                      "valeur entière")
            if days_number < 1:
                raise exceptions.UndersizedArgument(days_number, 1)
            elif days_number > 1000:
                raise exceptions.OversizedArgument(days_number, 1000)
        elif utils.is_option_enabled(options, 'time',
                                     has_value=True):  # No value assigned
            raise exceptions.MisformattedArgument(time_option,
                                                  "valeur entière")
        else:  # Option not used
            days_number = default_days_number

        # Determine granularity
        granularity = None
        for granularity_option in ('hour', 'day', 'month', 'year'):
            if utils.is_option_enabled(options, granularity_option):
                granularity = granularity_option
                break
        if not granularity:
            granularity = 'hour' if days_number < Server.HOURS_GRANULARITY_LIMIT \
                else 'day' if days_number < Server.DAYS_GRANULARITY_LIMIT \
                else 'month' if days_number < Server.MONTHS_GRANULARITY_LIMIT \
                else 'year'
        return days_number, granularity
Esempio n. 3
0
    async def switch(self,
                     context,
                     dest_channel: discord.TextChannel,
                     *,
                     options=""):
        if context.channel == dest_channel \
                or not context.author.permissions_in(dest_channel).send_messages:
            raise exceptions.ForbiddenChannel(dest_channel)
        number_option = utils.get_option_value(options, 'number')
        if number_option is not None:  # Value assigned
            try:
                messages_number = int(number_option)
            except ValueError:
                raise exceptions.MisformattedArgument(number_option,
                                                      "valeur entière")
            if messages_number < 0:
                raise exceptions.UndersizedArgument(messages_number, 0)
            elif messages_number > 10 and not checker.has_any_mod_role(
                    context, print_error=False):
                raise exceptions.OversizedArgument(messages_number, 10)
        elif utils.is_option_enabled(options, 'number',
                                     has_value=True):  # No value assigned
            raise exceptions.MisformattedArgument(number_option,
                                                  "valeur entière")
        else:  # Option not used
            messages_number = 3
        do_ping = utils.is_option_enabled(options, 'ping')
        do_delete = utils.is_option_enabled(options, 'delete')
        if do_ping or do_delete:
            checker.has_any_mod_role(context, print_error=True)

        messages = await context.channel.history(limit=messages_number + 1
                                                 ).flatten()
        messages.reverse()  # Order by oldest first
        messages.pop()  # Remove message used for the command

        await context.message.delete()
        if not do_delete:
            await context.send(
                f"**Veuillez basculer cette discussion dans le canal {dest_channel.mention} qui "
                f"serait plus approprié !** 🧹")
        else:
            await context.send(
                f"**La discussion a été déplacée dans le canal {dest_channel.mention} !** "
                f"({len(messages)} messages supprimés) 🧹")
        if messages_number != 0:
            await dest_channel.send(
                f"**Suite de la discussion de {context.channel.mention}** 💨"
            )
        await self.move_messages(messages, dest_channel, do_ping, do_delete)
Esempio n. 4
0
    async def automessage_print(self, context, automessage_id: int, *, options=""):
        automessages_data = zbot.db.load_automessages({'automessage_id': automessage_id}, ['message'])
        if not automessages_data:
            raise exceptions.UnknownAutomessage(automessage_id)

        raw_text = utils.is_option_enabled(options, 'raw')
        message = automessages_data[0]['message']
        await context.send(message if not raw_text else f"`{message}`")
Esempio n. 5
0
    async def setup(self,
                    context: commands.Context,
                    announce: str,
                    dest_channel: discord.TextChannel,
                    emoji: typing.Union[discord.Emoji, str],
                    nb_winners: int,
                    time: converter.to_datetime,
                    *,
                    options=""):
        # Check arguments
        if not context.author.permissions_in(dest_channel).send_messages:
            raise exceptions.ForbiddenChannel(dest_channel)
        if isinstance(emoji, str) and emojis.emojis.count(emoji) != 1:
            raise exceptions.ForbiddenEmoji(emoji)
        if nb_winners < 1:
            raise exceptions.UndersizedArgument(nb_winners, 1)
        if (time - utils.get_current_time()).total_seconds() <= 0:
            argument_size = converter.humanize_datetime(time)
            min_argument_size = converter.humanize_datetime(
                utils.get_current_time())
            raise exceptions.UndersizedArgument(argument_size,
                                                min_argument_size)

        # Run command
        organizer = context.author
        do_announce = not utils.is_option_enabled(options, 'no-announce')
        prefixed_announce = utils.make_announce(
            context.guild, announce, do_announce and self.ANNOUNCE_ROLE_NAME)
        embed = self.build_announce_embed(emoji, nb_winners, organizer, time,
                                          self.guild.roles)
        message = await dest_channel.send(prefixed_announce, embed=embed)
        await message.add_reaction(emoji)

        # Register data
        job_id = scheduler.schedule_stored_job(self.JOBSTORE, time,
                                               self.run_lottery, message.id).id
        lottery_data = {
            'lottery_id': self.get_next_lottery_id(),
            'message_id': message.id,
            'channel_id': dest_channel.id,
            'emoji_code': emoji if isinstance(emoji, str) else emoji.id,
            'nb_winners': nb_winners,
            'organizer_id': organizer.id,
        }
        zbot.db.update_job_data(self.JOBSTORE, job_id, lottery_data)
        # Add data managed by scheduler later to avoid updating the database with them
        lottery_data.update({
            '_id': job_id,
            'next_run_time': converter.to_timestamp(time)
        })
        self.pending_lotteries[message.id] = lottery_data

        # Confirm command
        await context.send(
            f"Tirage au sort d'identifiant `{lottery_data['lottery_id']}` programmé : <{message.jump_url}>."
        )
Esempio n. 6
0
    async def emojis(self,
                     context: commands.Context,
                     poll_id: int,
                     emoji_list: converter.to_emoji_list,
                     *,
                     options=""):
        message, channel, previous_emoji_list, is_exclusive, required_role_name, time, organizer = \
            await self.get_message_env(poll_id, raise_if_not_found=True)

        if context.author != organizer:
            checker.has_any_mod_role(context, print_error=True)
        if not context.author.permissions_in(channel).send_messages:
            raise exceptions.ForbiddenChannel(channel)
        if not emoji_list:
            raise commands.MissingRequiredArgument(
                context.command.params['emoji_list'])
        if len(emoji_list) > 20:
            raise exceptions.OversizedArgument(f"{len(emoji_list)} emojis",
                                               "20 emojis")
        required_role_name = utils.get_option_value(options, 'role')
        if required_role_name:
            utils.try_get(  # Raise if role does not exist
                self.guild.roles,
                error=exceptions.UnknownRole(required_role_name),
                name=required_role_name)

        is_exclusive = utils.is_option_enabled(options, 'exclusive')
        previous_reactions = [
            utils.try_get(message.reactions,
                          error=exceptions.MissingEmoji(previous_emoji),
                          emoji=previous_emoji)
            for previous_emoji in previous_emoji_list
        ]
        for previous_reaction in previous_reactions:
            await previous_reaction.remove(zbot.bot.user)
        for emoji in emoji_list:
            await message.add_reaction(emoji)
        embed = self.build_announce_embed(message.embeds[0].description,
                                          is_exclusive, required_role_name,
                                          organizer, time, self.guild.roles)
        await message.edit(embed=embed)

        job_id = self.pending_polls[message.id]['_id']
        poll_data = {
            'emoji_codes':
            list(map(lambda e: e if isinstance(e, str) else e.id, emoji_list)),
            'is_exclusive':
            is_exclusive,
            'required_role_name':
            required_role_name
        }
        zbot.db.update_poll_data(job_id, poll_data)
        self.pending_polls[message.id].update(poll_data)
        await context.send(
            f"Émojis du sondage d'identifiant `{poll_id}` mis à jour : <{message.jump_url}>"
        )
Esempio n. 7
0
    async def simulate(self,
                       context: commands.Context,
                       src_channel: discord.TextChannel,
                       message_id: int,
                       emoji_list: converter.to_emoji_list = (),
                       dest_channel: discord.TextChannel = None,
                       *,
                       options=""):
        if dest_channel and not context.author.permissions_in(
                dest_channel).send_messages:
            raise exceptions.ForbiddenChannel(dest_channel)
        is_exclusive = utils.is_option_enabled(options, 'exclusive')
        required_role_name = utils.get_option_value(options, 'role')
        if required_role_name:
            utils.try_get(  # Raise if role does not exist
                self.guild.roles,
                error=exceptions.UnknownRole(required_role_name),
                name=required_role_name)

        message = await utils.try_get_message(
            src_channel,
            message_id,
            error=exceptions.MissingMessage(message_id))
        if not emoji_list:
            if len(message.reactions) == 0:
                raise exceptions.MissingConditionalArgument(
                    "Une liste d'émojis doit être fournie si le message ciblé n'a pas de réaction."
                )
            else:
                emoji_list = [reaction.emoji for reaction in message.reactions]
        elif len(emoji_list) > 20:
            raise exceptions.OversizedArgument(f"{len(emoji_list)} emojis",
                                               "20 emojis")

        reactions, results = await Poll.count_votes(message, emoji_list,
                                                    is_exclusive,
                                                    required_role_name)
        announcement = await (
            dest_channel
            or context).send("Évaluation des votes sur base des réactions.")
        await Poll.announce_results(results,
                                    message,
                                    announcement.channel,
                                    is_exclusive,
                                    required_role_name,
                                    dest_message=announcement)
Esempio n. 8
0
File: bot.py Progetto: Zedd7/ZBot
 async def version(self, context, *, options=''):
     bot_display_name = self.get_bot_display_name(self.user, self.guild)
     if not utils.is_option_enabled(options,
                                    'all'):  # Display current version
         current_version = zbot.__version__
         embed = discord.Embed(
             title=f"Version actuelle de @{bot_display_name}",
             color=self.EMBED_COLOR)
         embed.add_field(name="Numéro", value=f"**{current_version}**")
         if changelog := self.get_changelog(current_version):
             date_iso, description, _ = changelog
             embed.add_field(name="Date",
                             value=converter.to_human_format(
                                 converter.to_datetime(date_iso)))
             embed.add_field(name="Description",
                             value=description,
                             inline=False)
         embed.set_footer(
             text="Utilisez +changelog <version> pour plus d'informations")
         await context.send(embed=embed)
Esempio n. 9
0
File: admin.py Progetto: Zedd7/ZBot
    async def report_recruitment(self,
                                 context,
                                 target: typing.Union[discord.Member, int],
                                 *,
                                 options=""):
        recruitment_channel = self.guild.get_channel(
            self.RECRUITMENT_CHANNEL_ID)
        if isinstance(target, discord.Member):
            author = target
            all_recruitment_announces = await recruitment_channel.history(
                oldest_first=False).flatten()
            recruitment_announces = list(
                filter(lambda a: a.author == author,
                       all_recruitment_announces))
            last_recruitment_announce = recruitment_announces and recruitment_announces[
                0]
        else:
            announce_id = target
            last_recruitment_announce = await utils.try_get_message(
                recruitment_channel,
                announce_id,
                error=exceptions.MissingMessage(announce_id))
            recruitment_announces = [last_recruitment_announce]
            author = last_recruitment_announce.author
        clear = utils.is_option_enabled(options, 'clear')

        if not last_recruitment_announce:
            raise exceptions.MissingMessage(missing_message_id=None)

        # Run checks
        patched_context = copy(context)
        patched_context.send = self.mock_send
        self.send_buffer.clear()
        await self.check_authors_clan_contact_role(patched_context, [last_recruitment_announce]) \
            or self.send_buffer.pop()
        await self.check_recruitment_announces_uniqueness(patched_context, recruitment_announces) \
            or self.send_buffer.pop()
        await self.check_recruitment_announces_length(patched_context, [last_recruitment_announce]) \
            or self.send_buffer.pop()
        await self.check_recruitment_announces_embeds(patched_context, [last_recruitment_announce]) \
            or self.send_buffer.pop()
        await self.check_recruitment_announces_timespan(
            patched_context, recruitment_channel,
            [last_recruitment_announce]) or self.send_buffer.pop()

        if not self.send_buffer:
            await context.send(
                f"L'annonce ne présente aucun problème. :ok_hand: ")
        else:
            # DM author
            await utils.try_dm(
                author,
                f"Bonjour. Il a été détecté que ton annonce de recrutement ne respectait pas le "
                f"règlement du serveur. Voici un rapport de l'analyse effectuée: \n _ _"
            )
            await utils.try_dms(author, self.send_buffer, group_in_blocks=True)
            await utils.try_dm(
                author, f"_ _ \n"
                f"En attendant que le problème soit réglé, ton annonce a été supprimée.\n"
                f"En cas de besoin, tu peux contacter {context.author.mention} qui a reçu une copie du "
                f"rapport d'analyse.\n"
                f"Pour éviter ce genre de désagrément, vérifie que ton annonce respecte le règlement en utilisant la "
                f"commande `+valider annonce` dans le canal <#557870289292230666>.\n _ _"
            )
            await utils.try_dm(
                author, f"Copie du contenu de l'annonce:\n _ _ \n"
                f">>> {last_recruitment_announce.content}")

            # DM moderator
            await utils.try_dm(
                context.author,
                f"Rapport d'analyse envoyé à {author.mention}: \n _ _")
            await utils.try_dms(context.author,
                                self.send_buffer,
                                group_in_blocks=True)
            await utils.try_dm(
                context.author, f"_ _ \n"
                f"Copie du contenu de l'annonce:\n _ _ \n"
                f">>> {last_recruitment_announce.content}")

            # Delete announce
            await last_recruitment_announce.delete()
            await context.send(
                f"L'annonce a été supprimée et un rapport envoyé par MP. :ok_hand: "
            )

            # Clear announce tracking records
            if clear:
                await self.clear_recruitment(context, author)
Esempio n. 10
0
File: admin.py Progetto: Zedd7/ZBot
    async def inspect_recruitment(self,
                                  context,
                                  member: typing.Union[discord.Member,
                                                       str] = None,
                                  *,
                                  options=''):
        """Post the status of the recruitment announces monitoring."""
        if isinstance(member,
                      str):  # Option mistakenly captured as member name
            options += f" {member}"
            member = None
        require_contact_role = not utils.is_option_enabled(options, 'all')
        recruitment_channel = self.guild.get_channel(
            self.RECRUITMENT_CHANNEL_ID)
        zbot.db.update_recruitment_announces(
            await recruitment_channel.history().flatten())

        # Get the record of each author's last announce (deleted or not)
        author_last_announce_data = {}
        for announce_data in zbot.db.load_recruitment_announces_data(
                query={'author': member.id} if member else {},
                order=[('time', -1)]):
            # Associate each author with his/her last announce data
            if announce_data['author'] not in author_last_announce_data:
                author_last_announce_data[announce_data['author']] = {
                    'last_announce_time': announce_data['time'],
                    'message_id': announce_data['_id']
                }

        # Enhance announces data with additional information
        min_timespan = datetime.timedelta(
            # Apply a tolerance of 2 days for players interpreting the 30 days range as "one month".
            # This is a subtraction because the resulting value is the number of days to wait before posting again.
            days=self.MIN_RECRUITMENT_ANNOUNCE_TIMESPAN -
            self.RECRUITMENT_ANNOUNCE_TIMESPAN_TOLERANCE)
        today = utils.bot_tz_now()
        for author_id, announce_data in author_last_announce_data.items():
            last_announce_time_localized = converter.to_utc(
                announce_data['last_announce_time'])
            next_announce_time_localized = last_announce_time_localized + min_timespan
            author_last_announce_data[author_id] = {
                'last_announce_time': last_announce_time_localized,
                'next_announce_time': next_announce_time_localized,
                'is_time_elapsed': next_announce_time_localized <= today
            }

        # Bind the member to the announce data, filter, and order by date asc
        member_announce_data = {
            self.guild.get_member(author_id): _
            for author_id, _ in author_last_announce_data.items()
        }
        filtered_member_announce_data = {
            author: _
            for author, _ in member_announce_data.items()
            if author is not None  # Still member of the server
            and
            not checker.has_any_mod_role(context, author, print_error=False
                                         )  # Ignore moderation messages
            and (not require_contact_role
                 or checker.has_role(author, Stats.CLAN_CONTACT_ROLE_NAME))
        }
        ordered_member_announce_data = sorted(
            filtered_member_announce_data.items(),
            key=lambda elem: elem[1]['last_announce_time'])

        # Post the status of announces data
        if ordered_member_announce_data:
            await context.send("Statut du suivi des annonces de recrutement :")
            for block in utils.make_message_blocks([
                    f"• {author.mention} : {converter.to_human_format(announce_data['last_announce_time'])} "
                    +
                ("✅" if announce_data['is_time_elapsed'] else
                 f"⏳ (→ {converter.to_human_format(announce_data['next_announce_time'])})"
                 ) for author, announce_data in ordered_member_announce_data
            ]):
                await context.send(block)
        else:
            await context.send("Aucun suivi enregistré.")
Esempio n. 11
0
    async def start(self,
                    context: commands.Context,
                    announce: str,
                    description: str,
                    dest_channel: discord.TextChannel,
                    emoji_list: converter.to_emoji_list,
                    time: converter.to_future_datetime,
                    *,
                    options=""):
        # Check arguments
        if not context.author.permissions_in(dest_channel).send_messages:
            raise exceptions.ForbiddenChannel(dest_channel)
        if not emoji_list:
            raise commands.MissingRequiredArgument(
                context.command.params['emoji_list'])
        if len(emoji_list) > 20:
            raise exceptions.OversizedArgument(f"{len(emoji_list)} emojis",
                                               "20 emojis")
        do_announce = utils.is_option_enabled(options, 'do-announce')
        do_pin = utils.is_option_enabled(options, 'pin')
        if do_announce or do_pin:
            checker.has_any_mod_role(context, print_error=True)
        required_role_name = utils.get_option_value(options, 'role')
        if required_role_name:
            utils.try_get(  # Raise if role does not exist
                self.guild.roles,
                error=exceptions.UnknownRole(required_role_name),
                name=required_role_name)

        # Run command
        is_exclusive = utils.is_option_enabled(options, 'exclusive')
        organizer = context.author
        prefixed_announce = utils.make_announce(
            context.guild, announce, do_announce and self.ANNOUNCE_ROLE_NAME)
        embed = self.build_announce_embed(description, is_exclusive,
                                          required_role_name, organizer, time,
                                          self.guild.roles)
        message = await dest_channel.send(prefixed_announce, embed=embed)
        for emoji in emoji_list:
            await message.add_reaction(emoji)
        if do_pin:
            await message.pin()

        # Register data
        job_id = scheduler.schedule_stored_job(
            zbot.db.PENDING_POLLS_COLLECTION, time, self.close_poll,
            message.id).id
        poll_data = {
            'poll_id':
            self.get_next_poll_id(),
            'message_id':
            message.id,
            'channel_id':
            dest_channel.id,
            'emoji_codes':
            list(map(lambda e: e if isinstance(e, str) else e.id, emoji_list)),
            'organizer_id':
            organizer.id,
            'is_exclusive':
            is_exclusive,
            'required_role_name':
            required_role_name,
        }
        zbot.db.update_poll_data(job_id, poll_data)
        # Add data managed by scheduler later to avoid updating the database with them
        poll_data.update({
            '_id': job_id,
            'next_run_time': converter.to_timestamp(time)
        })
        self.pending_polls[message.id] = poll_data

        # Confirm command
        await context.send(
            f"Sondage d'identifiant `{poll_data['poll_id']}` programmé : <{message.jump_url}>."
        )
Esempio n. 12
0
File: server.py Progetto: Zedd7/ZBot
    async def graph_messages(self, context, *, options=""):
        days_number, granularity = await self.parse_time_arguments(
            options, default_days_number=2)
        do_split = utils.is_option_enabled(options, 'split')

        # Load, compute and reshape data
        today = utils.community_tz_now()
        time_limit = today - datetime.timedelta(days=days_number)
        message_counts_data = zbot.db.load_message_counts(
            {'time': {
                '$gt': converter.to_utc(time_limit)
            }}, ['time', 'count', 'channel_id'])
        times_by_channel, counts_by_channel = {}, {}
        if granularity == 'hour':  # Plot the time and exact value to place the dot accurately
            for data in message_counts_data:
                localized_time = converter.to_community_tz(
                    converter.to_utc(data['time'])).replace(tzinfo=None)
                times_by_channel.setdefault(data['channel_id'],
                                            []).append(localized_time)
                counts_by_channel.setdefault(data['channel_id'],
                                             []).append(data['count'])
        elif granularity in (
                'day', 'month', 'year'
        ):  # Only plot the date, and average value to align with the tick
            counts_by_channel_and_time = {}
            for data in message_counts_data:
                localized_time = converter.to_community_tz(
                    converter.to_utc(data['time']))
                counts_by_channel_and_time.setdefault(data['channel_id'], {}) \
                    .setdefault(localized_time.date(), []).append(data['count'])
            for channel, counts_by_date in counts_by_channel_and_time.items():
                times_by_channel[channel] = list(counts_by_date.keys())
                counts_by_channel[channel] = [
                    round(sum(date_counts) / len(date_counts))
                    for date_counts in counts_by_date.values()
                ]

        min_count, max_count = float('inf'), -float('inf')
        if do_split:
            for channel_id, times, channel_counts in zip(
                    times_by_channel.keys(), times_by_channel.values(),
                    counts_by_channel.values()):
                channel_name = self.guild.get_channel(channel_id).name
                plt.plot(times,
                         channel_counts,
                         label=f"#{channel_name}",
                         linestyle='-',
                         marker='.',
                         alpha=0.75)
                plt.legend()
                if min(channel_counts) < min_count:
                    min_count = min(channel_counts)
                if max(channel_counts) > max_count:
                    max_count = max(channel_counts)
        else:
            times = list(times_by_channel.values())[0]
            counts = [0] * len(times)
            for channel_counts in counts_by_channel.values():
                for time_index, channel_count in enumerate(channel_counts):
                    counts[time_index] += channel_count
            plt.plot(times, counts, linestyle='-', marker='.', alpha=0.75)
            min_count, max_count = min(counts), max(counts)

        self.configure_plot(days_number, time_limit, today, min_count,
                            max_count, "Nombre de messages horaires",
                            granularity)
        await context.send(file=self.render_graph())