コード例 #1
0
    async def send_automessage(self):
        # Check if not running above frequency
        now = utils.bot_tz_now()
        last_automessage_date = zbot.db.get_metadata('last_automessage_date')
        if last_automessage_date:
            last_automessage_date_localized = converter.to_utc(last_automessage_date)
            if not utils.is_time_almost_elapsed(last_automessage_date_localized, now, self.AUTOMESSAGE_FREQUENCY):
                logger.debug(f"Prevented sending automessage because running above defined frequency.")
                return

        # Get automessages data
        automessages_data = zbot.db.load_automessages(
            {'automessage_id': {
                '$ne': zbot.db.get_metadata('last_automessage_id')  # Don't post the same message twice in a row
            }},
            ['automessage_id', 'channel_id', 'message']
        )
        if not automessages_data:  # At most a single automessage exists
            automessages_data = zbot.db.load_automessages(  # Load it anyway
                {}, ['automessage_id', 'channel_id', 'message']
            )
        if not automessages_data:  # Not automessage exists
            return  # Abort
        automessage_data = random.choice(automessages_data)
        automessage_id = automessage_data['automessage_id']
        message = automessage_data['message']
        channel = self.guild.get_channel(automessage_data['channel_id'])
        last_channel_message = (await channel.history(limit=1).flatten())[0]

        # Run halt checks on target channel
        if last_channel_message.author == self.user:  # Avoid spamming an channel
            # Check if the cooldown between two bot messages has expired
            last_channel_message_date_localized = converter.to_utc(last_channel_message.created_at)
            cooldown_expired = last_channel_message_date_localized < now - self.AUTOMESSAGE_COOLDOWN
            if not cooldown_expired:
                logger.debug(f"Skipped automessage of id {automessage_id} as cooldown has not expired yet.")
                return
        else:  # Avoid interrupting conversations
            is_channel_quiet, attempt_count = False, 0
            while not is_channel_quiet:  # Wait for the channel to be quiet to send the message
                now = utils.bot_tz_now()
                last_channel_message_date_localized = converter.to_utc(last_channel_message.created_at)
                is_channel_quiet = last_channel_message_date_localized < now - self.AUTOMESSAGE_WAIT
                if not is_channel_quiet:
                    attempt_count += 1
                    if attempt_count < 3:
                        logger.debug(
                            f"Pausing automessage of id {automessage_id} for {self.AUTOMESSAGE_WAIT.seconds} "
                            f"seconds while waiting for quietness in target channel."
                        )
                        await asyncio.sleep(self.AUTOMESSAGE_WAIT.seconds)  # Sleep for the duration of the delay
                    else:  # After 3 failed attempts, skip
                        logger.debug(f"Skipped automessage of id {automessage_id} after 3 waits for quietness.")
                        return

        # All checks passed, send the automessage
        zbot.db.update_metadata('last_automessage_id', automessage_id)
        zbot.db.update_metadata('last_automessage_date', now)
        await channel.send(message)
コード例 #2
0
    async def celebrate_account_anniversaries(self):
        # Check if not running above frequency
        today = utils.bot_tz_now()
        last_anniversaries_celebration = zbot.db.get_metadata('last_anniversaries_celebration')
        if last_anniversaries_celebration:
            last_anniversaries_celebration_localized = converter.to_utc(last_anniversaries_celebration)
            if last_anniversaries_celebration_localized.date() == converter.to_utc(today).date():
                logger.debug(f"Prevented sending anniversaries celebration because running above defined frequency.")
                return

        # Get anniversary data
        self.record_account_creation_dates()
        account_anniversaries = zbot.db.get_anniversary_account_ids(
            today, self.MIN_ACCOUNT_CREATION_DATE
        )
        member_anniversaries = {}
        for years, account_ids in account_anniversaries.items():
            for account_id in account_ids:
                member = self.guild.get_member(account_id)
                if member and checker.has_role(member, Messaging.PLAYER_ROLE_NAME):
                    member_anniversaries.setdefault(years, []).append(member)

        # Remove celebration emojis in names from previous anniversaries
        for member in self.guild.members:
            if self.CELEBRATION_EMOJI in member.display_name:
                try:
                    await member.edit(
                        nick=member.display_name.replace(self.CELEBRATION_EMOJI, '').rstrip()
                    )
                except (discord.Forbidden, discord.HTTPException):
                    pass

        # Add celebration emojis for today's anniversaries
        for year, members in member_anniversaries.items():
            for member in members:
                try:
                    await member.edit(
                        nick=member.display_name + " " + self.CELEBRATION_EMOJI * year
                    )
                except (discord.Forbidden, discord.HTTPException):
                    pass

        # Announce anniversaries (after updating names to refresh the cache)
        celebration_channel = self.guild.get_channel(self.CELEBRATION_CHANNEL_ID)
        if member_anniversaries:
            await celebration_channel.send("**Voici les anniversaires du jour !** 🎂")
            for year in sorted(member_anniversaries.keys(), reverse=True):
                for member in member_anniversaries[year]:
                    await celebration_channel.send(
                        f"  • {member.mention} fête ses **{year}** ans sur World of Tanks ! 🥳"
                    )

        zbot.db.update_metadata('last_anniversaries_celebration', today)
コード例 #3
0
ファイル: admin.py プロジェクト: Zedd7/ZBot
    async def check_recruitment_announces_timespan(context, channel,
                                                   announces):
        """Check that no announce is re-posted before a given timespan."""
        zbot.db.update_recruitment_announces(await channel.history().flatten())

        # Get records of all deleted announces
        # Still existing announces are handled by Admin.check_recruitment_announces_uniqueness
        author_last_announce_data = {}
        for announce_data in zbot.db.load_recruitment_announces_data(
                query={'_id': {
                    '$nin': list(map(lambda a: a.id, announces))
                }},
                order=[('time', -1)],
        ):
            # Associate each author with his/her last delete announce data
            if announce_data['author'] not in author_last_announce_data:
                author_last_announce_data[announce_data['author']] = {
                    'time': announce_data['time'],
                    'message_id': announce_data['_id']
                }

        # Find all existing announces that have the same author as a recent (but deleted) announce
        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=Admin.MIN_RECRUITMENT_ANNOUNCE_TIMESPAN -
            Admin.RECRUITMENT_ANNOUNCE_TIMESPAN_TOLERANCE)
        before_timespan_announces = []
        for announce in announces:
            if announce_data := author_last_announce_data.get(
                    announce.author.id):
                previous_announce_time_localized = converter.to_utc(
                    announce_data['time'])
                if previous_announce_time_localized \
                        <= converter.to_utc(announce.created_at) \
                        < previous_announce_time_localized + min_timespan:
                    before_timespan_announces.append(
                        (announce, previous_announce_time_localized))
コード例 #4
0
ファイル: server.py プロジェクト: Zedd7/ZBot
 async def record_message_count(self, time):
     message_counts = []
     for channel_name in self.DISCUSSION_CHANNELS:
         channel = utils.try_get(self.guild.channels, name=channel_name)
         channel_message_count = len(await channel.history(
             after=converter.to_utc(time -
                                    datetime.timedelta(hours=1)).replace(
                                        tzinfo=None),
             limit=999).flatten())
         message_counts.append({
             'count': channel_message_count,
             'channel_id': channel.id
         })
     zbot.db.insert_timed_message_counts(time, message_counts)
コード例 #5
0
ファイル: server.py プロジェクト: Zedd7/ZBot
    async def graph_members(self, context, *, options=""):
        days_number, granularity = await self.parse_time_arguments(options)

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

        plt.plot(times, counts, linestyle='-', marker='.', alpha=0.75)

        self.configure_plot(days_number, time_limit, today, min(counts),
                            max(counts), "Nombre de membres", granularity)
        await context.send(file=self.render_graph())
コード例 #6
0
ファイル: server.py プロジェクト: Zedd7/ZBot
    async def record_server_stats(self):
        now = utils.bot_tz_now()
        last_server_stats_record_date = zbot.db.get_metadata(
            'last_server_stats_record')
        if last_server_stats_record_date:
            last_server_stats_record_date_localized = converter.to_utc(
                last_server_stats_record_date)
            if not utils.is_time_almost_elapsed(
                    last_server_stats_record_date_localized,
                    now,
                    self.SERVER_STATS_RECORD_FREQUENCY,
                    tolerance=datetime.timedelta(minutes=5)):
                logger.debug(
                    f"Prevented recording server stats because running above define frequency."
                )
                return

        await self.record_member_count(now)
        await self.record_message_count(now)
        zbot.db.update_metadata('last_server_stats_record', now)
コード例 #7
0
ファイル: special.py プロジェクト: Zedd7/ZBot
    async def validate_recruitments(self, context):
        if not checker.has_guild_role(context.guild, context.author,
                                      Stats.CLAN_CONTACT_ROLE_NAME):
            raise exceptions.MissingRoles([Stats.CLAN_CONTACT_ROLE_NAME])

        recruitment_channel = self.guild.get_channel(
            Admin.RECRUITMENT_CHANNEL_ID)
        all_recruitment_announces = await recruitment_channel.history(
        ).flatten()
        recruitment_announces = list(
            filter(lambda a: a.author == context.author,
                   all_recruitment_announces))
        last_recruitment_announce = recruitment_announces and recruitment_announces[
            0]

        if not last_recruitment_announce:
            await context.send("Aucune annonce de recrutement n'a été trouvée."
                               )
        else:
            patched_context = copy(context)
            patched_context.send = self.mock_send
            validation_succeeded = True
            if await Admin.check_recruitment_announces_uniqueness(
                    patched_context, recruitment_announces):
                validation_succeeded = False
                await context.send(
                    f"Tu as publié {len(recruitment_announces)} annonces. Une seule est autorisée à la fois."
                )
            if await Admin.check_recruitment_announces_length(
                    patched_context, [last_recruitment_announce]):
                validation_succeeded = False
                apparent_length = Admin.compute_apparent_length(
                    last_recruitment_announce)
                await context.send(
                    f"L'annonce est d'une longueur apparente de **{apparent_length}** caractères (max "
                    f"{Admin.MAX_RECRUITMENT_ANNOUNCE_LENGTH}). Réduit sa longueur en retirant du contenu ou en "
                    f"réduisant le nombre de sauts de lignes.")
            if await Admin.check_recruitment_announces_embeds(
                    patched_context, [last_recruitment_announce]):
                validation_succeeded = False
                await context.send(
                    f"Ton annonce contient un embed, ce qui n'est pas autorisé. Utilise un raccourcisseur d'URLs comme "
                    f"<https://tinyurl.com> pour héberger tes liens.")
            if await Admin.check_recruitment_announces_timespan(
                    patched_context, recruitment_channel,
                [last_recruitment_announce]):
                validation_succeeded = False
                await context.send(
                    f"Ton annonce a été postée avant le délai minimum de {Admin.MIN_RECRUITMENT_ANNOUNCE_TIMESPAN} "
                    f"jours entre deux annonces.")

            if validation_succeeded:
                await context.send(
                    f"L'annonce ne présente aucun problème. :ok_hand: ")

            last_announce_time_localized = converter.to_utc(
                zbot.db.load_recruitment_announces_data(
                    query={'author': context.author.id},
                    order=[('time', -1)])[0]['time'])
            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=Admin.MIN_RECRUITMENT_ANNOUNCE_TIMESPAN -
                Admin.RECRUITMENT_ANNOUNCE_TIMESPAN_TOLERANCE)
            next_announce_time_localized = last_announce_time_localized + min_timespan
            await context.send(
                f"Tu pourras à nouveau poster une annonce à partir du "
                f"{converter.to_human_format(next_announce_time_localized)}.")
コード例 #8
0
ファイル: admin.py プロジェクト: 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é.")
コード例 #9
0
ファイル: server.py プロジェクト: 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())