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}>")
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
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)
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}`")
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}>." )
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}>" )
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)
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)
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)
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é.")
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}>." )
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())