async def time(self, ctx: commands.Context, reminder_id: int, *, time: str): """Modify the time of an existing reminder.""" users_reminders = await self.get_user_reminders(ctx.message.author.id) edit_reminder = self.get_reminder(users_reminders, reminder_id) if not edit_reminder: await self.send_non_existant_msg(ctx, reminder_id) return try: time_delta = parse_timedelta(time, minimum=timedelta(minutes=1)) if not time_delta: await ctx.send_help() return except commands.BadArgument as ba: await self.send_message(ctx, str(ba)) return seconds = time_delta.total_seconds() future = int(current_time.time() + seconds) future_text = humanize_timedelta(timedelta=time_delta) reminder = { "USER_REMINDER_ID": reminder_id, "USER_ID": edit_reminder["USER_ID"], "REMINDER": edit_reminder["REMINDER"], "FUTURE": future, "FUTURE_TEXT": future_text, } async with self.config.reminders() as current_reminders: current_reminders.remove(edit_reminder) current_reminders.append(reminder) await self.send_message( ctx, "Reminder with ID# **{}** has been edited successfully, and will now remind you {} from now." .format(reminder_id, future_text), )
def parse_td(cls, v, min=None, max=None): if not isinstance(v, str): raise TypeError("Not a valid timedelta") try: td = parse_timedelta(v, minimum=min, maximum=max) except BadArgument as e: raise TypeError(f"{e}") if td is None: raise TypeError("Not a valid timedelta") return td
def _check_heatpoint(*, author: discord.Member, action: Action, parameter: str): td = None try: td = parse_timedelta(parameter, maximum=datetime.timedelta(hours=24), minimum=datetime.timedelta(seconds=1), allowed_units=["hours", "minutes", "seconds"]) except BadArgument: pass if td is None: raise InvalidRule( f"`{action.value}` Invalid parameter. Must be between 1 second and 24 hours. " "You must specify `seconds`, `minutes` or `hours`")
async def _check_message_delete_after(*, cog, author: discord.Member, action: Action, parameter: str): td = None try: td = parse_timedelta(parameter, maximum=datetime.timedelta(minutes=1), minimum=datetime.timedelta(seconds=1), allowed_units=["minutes", "seconds"]) except BadArgument: pass if td is None: raise InvalidRule( f"`{action.value}` Invalid parameter. Must be between 1 second and 1 minute. " "You must specify `seconds` or `minutes`")
async def _check_heatpoints(*, cog, author: discord.Member, action: Action, parameter: list): if parameter[0] < 1 or parameter[0] > 100: raise InvalidRule( f"`{action.value}` Invalid parameter. You can only assign between 1 and 100 " "heatpoints.") td = None try: td = parse_timedelta(parameter[1], maximum=datetime.timedelta(hours=24), minimum=datetime.timedelta(seconds=1), allowed_units=["hours", "minutes", "seconds"]) except BadArgument: pass if td is None: raise InvalidRule( f"`{action.value}` Invalid parameter. Must be between 1 second and 24 hours. " "You must specify `seconds`, `minutes` or `hours`")
def _check_slowmode(*, author: discord.Member, action: Action, parameter: str): if not author.guild_permissions.manage_channels: raise InvalidRule( f"`{action.value}` You need `manage channels` permissions to make a rule with " "this action.") td = None try: td = parse_timedelta(parameter, maximum=datetime.timedelta(hours=6), minimum=datetime.timedelta(seconds=0), allowed_units=["hours", "minutes", "seconds"]) except BadArgument: pass if td is None: raise InvalidRule( f"`{action.value}` Invalid parameter. Must be between 1 second and 6 hours. " "You must specify `seconds`, `minutes` or `hours`. Can be `0 seconds` to " "deactivate slowmode.")
async def _check_custom_heatpoint(*, cog, author: discord.Member, action: Action, parameter: list): if not isinstance(parameter[0], str): raise InvalidRule( f"`{action.value}` Invalid parameter. The custom heat key must be a string." ) td = None try: td = parse_timedelta(parameter[1], maximum=datetime.timedelta(hours=24), minimum=datetime.timedelta(seconds=1), allowed_units=["hours", "minutes", "seconds"]) except BadArgument: pass if td is None: raise InvalidRule( f"`{action.value}` Invalid parameter. Must be between 1 second and 24 hours. " "You must specify `seconds`, `minutes` or `hours`")
async def create_reminder(self, ctx: commands.Context, time: str, text: str): """Logic to create a reminder.""" author = ctx.message.author maximum = await self.config.max_user_reminders() users_reminders = await self.get_user_reminders(author.id) if len(users_reminders) > maximum - 1: plural = "reminder" if maximum == 1 else "reminders" await self.send_message( ctx, ("You have too many reminders! " "I can only keep track of {} {} for you at a time.".format( maximum, plural)), ) return try: time_delta = parse_timedelta(time, minimum=timedelta(minutes=1)) if not time_delta: # Try again if the user is doing the old "[p]remindme 4 hours ..." format time_unit = text.split()[0] time = "{} {}".format(time, time_unit) text = text[len(time_unit):].strip() time_delta = parse_timedelta(time, minimum=timedelta(minutes=1)) if not text or not time_delta: await ctx.send_help() return except commands.BadArgument as ba: await self.send_message(ctx, str(ba)) return text = text.strip() if len(text) > 1000: await self.send_message(ctx, "Your reminder text is too long.") return seconds = time_delta.total_seconds() future = int(current_time.time() + seconds) future_text = humanize_timedelta(timedelta=time_delta) next_reminder_id = self.get_next_user_reminder_id(users_reminders) reminder = { "USER_REMINDER_ID": next_reminder_id, "USER_ID": author.id, "REMINDER": text, "FUTURE": future, "FUTURE_TEXT": future_text, } async with self.config.reminders() as current_reminders: current_reminders.append(reminder) await self.send_message( ctx, "I will remind you that in {}.".format(future_text)) can_react = ctx.channel.permissions_for(ctx.me).add_reactions can_edit = ctx.channel.permissions_for(ctx.me).manage_messages if can_react and can_edit: query: discord.Message = await ctx.send( "If anyone else would like to be reminded as well, click the bell below!" ) self.me_too_reminders[query.id] = reminder await query.add_reaction(self.reminder_emoji) await asyncio.sleep(30) await delete(query) del self.me_too_reminders[query.id]
async def do_actions(self, *, cog, user: discord.Member = None, message: discord.Message = None, guild: discord.Guild = None): if message and not user: user = message.author guild = guild if guild else user.guild channel: discord.Channel = message.channel if message else None templates_vars = { "action_name": self.name, "guild": str(guild), "guild_id": guild.id } if user: templates_vars.update({ "user": str(user), "user_name": user.name, "user_id": user.id, "user_mention": user.mention, "user_nickname": str(user.nick), "user_created_at": user.created_at, "user_joined_at": user.joined_at, }) if message: templates_vars["message"] = message.content templates_vars["message_id"] = message.id templates_vars["message_created_at"] = message.created_at templates_vars["message_link"] = message.jump_url if message.attachments: attachment = message.attachments[0] templates_vars["attachment_filename"] = attachment.filename templates_vars["attachment_url"] = attachment.url if channel: templates_vars["channel"] = str(channel) templates_vars["channel_name"] = channel.name templates_vars["channel_id"] = channel.id templates_vars["channel_mention"] = channel.mention last_expel_action = None for entry in self.actions: for action, value in entry.items(): action = Action(action) self.last_action = action if action == Action.DmUser: text = Template(value).safe_substitute(templates_vars) await user.send(text) elif action == Action.DeleteUserMessage: await message.delete() elif action == Action.NotifyStaff: text = Template(value).safe_substitute(templates_vars) await cog.send_notification(guild, text) elif action == Action.NotifyStaffAndPing: text = Template(value).safe_substitute(templates_vars) await cog.send_notification(guild, text, ping=True) elif action == Action.NotifyStaffWithEmbed: title, content = (value[0], value[1]) em = self._build_embed(title, content, templates_vars=templates_vars) await cog.send_notification(guild, "", embed=em) elif action == Action.SendInChannel: text = Template(value).safe_substitute(templates_vars) await channel.send(text) elif action == Action.SetChannelSlowmode: timedelta = parse_timedelta(value) await channel.edit(slowmode_delay=timedelta.seconds) elif action == Action.Dm: _id_or_name, content = (value[0], value[1]) user_to_dm = guild.get_member(_id_or_name) if not user_to_dm: user_to_dm = discord.utils.get(guild.members, name=_id_or_name) if not user_to_dm: continue content = Template(content).safe_substitute(templates_vars) try: await user_to_dm.send(content) except: # Should we care if the DM fails? pass elif action == Action.SendToChannel: _id_or_name, content = (value[0], value[1]) channel_dest = guild.get_channel(_id_or_name) if not channel_dest: channel_dest = discord.utils.get(guild.channels, name=_id_or_name) if not channel_dest: raise ExecutionError( f"Channel '{_id_or_name}' not found.") content = Template(content).safe_substitute(templates_vars) await channel_dest.send(content) elif action == Action.AddRolesToUser: to_assign = [] for role_id_or_name in value: role = guild.get_role(role_id_or_name) if role is None: role = discord.utils.get(guild.roles, name=role_id_or_name) if role: to_assign.append(role) to_assign = list(set(to_assign)) to_assign = [r for r in to_assign if r not in user.roles] if to_assign: await user.add_roles( *to_assign, reason=f"Assigned by Warden rule '{self.name}'") elif action == Action.RemoveRolesFromUser: to_unassign = [] for role_id_or_name in value: role = guild.get_role(role_id_or_name) if role is None: role = discord.utils.get(guild.roles, name=role_id_or_name) if role: to_unassign.append(role) to_unassign = list(set(to_unassign)) to_unassign = [r for r in to_unassign if r in user.roles] if to_unassign: await user.remove_roles( *to_unassign, reason=f"Unassigned by Warden rule '{self.name}'") elif action == Action.SetUserNickname: if value == "": value = None else: value = Template(value).safe_substitute(templates_vars) await user.edit( nick=value, reason=f"Changed nickname by Warden rule '{self.name}'" ) elif action == Action.BanAndDelete: if user not in guild.members: raise ExecutionError( f"User {user} ({user.id}) not in the server.") reason = f"Banned by Warden rule '{self.name}'" await guild.ban(user, delete_message_days=value, reason=reason) last_expel_action = ModAction.Ban cog.dispatch_event("member_remove", user, ModAction.Ban.value, reason) elif action == Action.Kick: if user not in guild.members: raise ExecutionError( f"User {user} ({user.id}) not in the server.") reason = f"Kicked by Warden action '{self.name}'" await guild.kick(user, reason=reason) last_expel_action = Action.Kick cog.dispatch_event("member_remove", user, ModAction.Kick.value, reason) elif action == Action.Softban: if user not in guild.members: raise ExecutionError( f"User {user} ({user.id}) not in the server.") reason = f"Softbanned by Warden rule '{self.name}'" await guild.ban(user, delete_message_days=1, reason=reason) await guild.unban(user) last_expel_action = Action.Softban cog.dispatch_event("member_remove", user, ModAction.Softban.value, reason) elif action == Action.Modlog: if last_expel_action is None: continue reason = Template(value).safe_substitute(templates_vars) await modlog.create_case( cog.bot, guild, utcnow(), last_expel_action.value, user, guild.me, reason, until=None, channel=None, ) elif action == Action.EnableEmergencyMode: if value: cog.emergency_mode[guild.id] = EmergencyMode( manual=True) else: try: del cog.emergency_mode[guild.id] except KeyError: pass elif action == Action.SendToMonitor: value = Template(value).safe_substitute(templates_vars) cog.send_to_monitor(guild, f"[Warden] ({self.name}): {value}") elif action == Action.NoOp: pass else: raise ExecutionError(f"Unhandled action '{self.name}'.") return bool(last_expel_action)
def _return_time(time): cooldown_time = parse_timedelta(time) if cooldown_time is None: return None return int(cooldown_time.total_seconds())
async def buttonpoll(self, ctx: commands.Context, chan: Optional[TextChannel] = None): channel = chan or ctx.channel assert isinstance(channel, TextChannel) # these two checks are untested :) if not channel.permissions_for( ctx.author).send_messages: # type:ignore return await ctx.send( f"You don't have permission to send messages in {channel.mention}, so I can't " "start a poll there.") if not channel.permissions_for(ctx.me).send_messages: # type:ignore return await ctx.send( f"I don't have permission to send messages in {channel.mention}, so I can't " "start a poll there.") try: # TITLE await ctx.send( f"I'll be creating a poll in {channel.mention}.\n" "What do you want the question to be? Keep it short, if you want to add more " "detail you can later on. 1 minute timeout, say `cancel` to cancel." ) t_msg: Message = await self.bot.wait_for( "message", check=MessagePredicate.same_context(ctx), timeout=60) if t_msg.content.lower() == "cancel": return await ctx.send("Cancelled.") if len(t_msg.content) > 256: return await ctx.send( "Question is too long, max 256 characters. Cancelled.") question = t_msg.content # DESCRIPTION await ctx.send( "Great! If you want an extended description, enter it now or say `skip` if you " "don't. 3 minute timeout.") d_msg: Message = await self.bot.wait_for( "message", check=MessagePredicate.same_context(ctx), timeout=180) if d_msg.content.lower() == "skip": await ctx.send("Okay, they'll be no description.") description = None elif d_msg.content.lower() == "cancel": return await ctx.send("Cancelled.") else: description = d_msg.content # OPTIONS await ctx.send( "What do you want the options to be? Enter up to five, seperated by a new line " "(on desktop do Shift+Enter). 3 minute timeout, say just `cancel` to cancel." ) o_msg: Message = await self.bot.wait_for( "message", check=MessagePredicate.same_context(ctx), timeout=180) if o_msg.content.lower() == "cancel": return await ctx.send("Cancelled.") str_options = o_msg.content.split("\n") if len(str_options) > 5: return await ctx.send("Too many options, max is 5. Cancelled.") elif len(str_options) < 2: return await ctx.send( "You need at least two options. Cancelled.") options: List[PollOption] = [] for str_option in str_options: if len(str_option) > 80: return await ctx.send( "One of your options is too long, the limit is 80 characters. Cancelled." ) if str_option in [i.name for i in options ]: # if in already added options return await ctx.send( "You can't have duplicate options. Cancelled.") option = PollOption(str_option, discord.ButtonStyle.primary) options.append(option) # TIME await ctx.send( "How long do you want the poll to be? Valid units are `seconds`, `minutes`, " "`hours`, `days` and `weeks`.\nExamples: `1 week` or `1 day 12 hours`" ) ti_msg: Message = await self.bot.wait_for( "message", check=MessagePredicate.same_context(ctx), timeout=60) try: duration = parse_timedelta(ti_msg.content) except Exception: return await ctx.send("Invalid time format. Cancelled.") if duration is None: return await ctx.send("Invalid time format. Cancelled.") # YES/NO QS change_vote = await wait_for_yes_no( ctx, content= ("Almost there! Just a few yes/no questions left.\nDo you want to allow " "people to change their vote while the poll is live?"), timeout=60, ) view_while_live = await wait_for_yes_no( ctx, content= "Do you want to allow people to view the results while the poll is live?", timeout=60, ) send_msg_when_over = await wait_for_press( ctx, items=[ PredItem(False, discord.ButtonStyle.primary, "Edit old"), PredItem(True, discord.ButtonStyle.primary, "Send new"), ], content= ("Do you want to send a new message when the poll is over, or just edit " "the old one? Note pie charts are only sent with `Send new`." ), timeout=60, ) except TimeoutError: return await ctx.send("Timed out, please start again.") await ctx.send("All done!") unique_poll_id = ( # msg ID and first 25 chars of sanitised question str(ctx.message.id) + "_" + "".join(c for c in question if c.isalnum())[:25]) poll_finish = datetime.datetime.now(datetime.timezone.utc) + duration poll = Poll( unique_poll_id=unique_poll_id, guild_id=ctx.guild.id, # type:ignore channel_id=ctx.channel.id, question=question, description=description, options=options, allow_vote_change=change_vote, view_while_live=view_while_live, send_msg_when_over=send_msg_when_over, poll_finish=poll_finish, cog=self, view=None, # type:ignore ) poll.view = PollView(self.config, poll) e = discord.Embed( colour=await self.bot.get_embed_color(ctx.channel), title=poll.question, description=poll.description or EmptyEmbed, ) e.add_field( name=(f"Ends at {datetime_to_timestamp(poll.poll_finish)}, " f"{datetime_to_timestamp(poll.poll_finish, 'R')}"), value= ("You have one vote, " + ("and you can change it by clicking a new button." if poll.allow_vote_change else "and you can't change it.") + ("\nYou can view the results while the poll is live, once you vote." if poll.view_while_live else "\nYou can view the results when the poll finishes.")), ) m = await ctx.send(embed=e, view=poll.view) poll.set_msg_id(m.id) async with self.config.guild( ctx.guild).poll_settings() as poll_settings: # type:ignore poll_settings[poll.unique_poll_id] = poll.to_dict() self.polls.append(poll)
def test_converter_timedelta(): assert converter.parse_timedelta("1 day") == datetime.timedelta(days=1) assert converter.parse_timedelta("1 minute") == datetime.timedelta( minutes=1) assert converter.parse_timedelta( "13 days 5 minutes") == datetime.timedelta(days=13, minutes=5)
async def parse(self, rule_str, cog, author=None): self.raw_rule = rule_str try: rule = yaml.safe_load(rule_str) except: raise InvalidRule( "Error parsing YAML. Please make sure the format " "is valid (a YAML validator may help)") if not isinstance(rule, dict): raise InvalidRule( f"This rule doesn't seem to follow the expected format.") if rule["name"] is None: raise InvalidRule("Rule has no 'name' parameter.") self.name = rule["name"].lower().replace(" ", "-") for key in rule.keys(): if key not in RULE_REQUIRED_KEYS and key not in RULE_FACULTATIVE_KEYS: raise InvalidRule(f"Unexpected key at root level: '{key}'.") for key in RULE_REQUIRED_KEYS: if key not in rule.keys(): raise InvalidRule(f"Missing key at root level: '{key}'.") if isinstance(rule["event"], list): try: for event in rule["event"]: self.events.append(Event(event)) except ValueError: raise InvalidRule(f"Invalid events.") else: try: self.events.append(Event(rule["event"])) except ValueError: raise InvalidRule("Invalid event.") if not self.events: raise InvalidRule("A least one event must be defined.") if Event.Periodic in self.events: # cog is None when running tests if cog and not await cog.config.wd_periodic_allowed(): raise InvalidRule( "The creation of periodic Warden rules is currently disabled. " "The bot owner must use '[p]dset warden periodicallowed' to " "enable them.") if "run-every" not in rule.keys(): raise InvalidRule( "The 'run-every' parameter is mandatory with " "periodic rules.") try: td = parse_timedelta(str(rule["run-every"]), maximum=datetime.timedelta(hours=24), minimum=datetime.timedelta(minutes=5), allowed_units=["hours", "minutes"]) if td is None: raise BadArgument() except BadArgument: raise InvalidRule( "The 'run-every' parameter must be between 5 minutes " "and 24 hours.") else: self.run_every = td self.next_run = utcnow() + td else: if "run-every" in rule.keys(): raise InvalidRule( "The 'periodic' event must be specified for rules with " "a 'run-every' parameter.") try: self.rank = Rank(rule["rank"]) except: raise InvalidRule("Invalid target rank. Must be 1-4.") try: priority = rule["priority"] if not isinstance(priority, int) or priority < 1 or priority > 999: raise InvalidRule( "Priority must be a number between 1 and 999.") self.priority = priority except KeyError: pass if "if" in rule: # TODO Conditions probably shouldn't be mandatory. if not isinstance(rule["if"], list): raise InvalidRule( "Invalid 'if' category. Must be a list of conditions.") else: raise InvalidRule("Rule must have at least one condition.") self.conditions = rule["if"] if not rule["if"]: raise InvalidRule("Rule must have at least one condition.") if not isinstance(rule["do"], list): raise InvalidRule("Invalid 'do' category. Must be a list of maps.") if not rule["do"]: raise InvalidRule("Rule must have at least one action.") self.actions = rule["do"] def is_condition_allowed_in_events(condition): for event in self.events: if not condition in ALLOWED_CONDITIONS[event]: return False return True def is_action_allowed_in_events(action): for event in self.events: if not action in ALLOWED_ACTIONS[event]: return False return True async def validate_condition(cond): condition = parameter = None for r, p in cond.items(): condition, parameter = r, p try: condition = Condition(condition) except ValueError: try: condition = ConditionBlock(condition) raise InvalidRule( f"Invalid: `{condition.value}` can only be at root level." ) except ValueError: suggestion = make_fuzzy_suggestion( condition, [c.value for c in Condition]) raise InvalidRule( f"Invalid condition: `{condition}`.{suggestion}") if not is_condition_allowed_in_events(condition): raise InvalidRule( f"Condition `{condition.value}` not allowed in the event(s) you have defined." ) _type = None for _type in CONDITIONS_PARAM_TYPE[condition]: if _type is None and parameter is None: break elif _type is None: continue if _type == list and isinstance(parameter, list): mandatory_arg_number = CONDITIONS_ARGS_N.get( condition, None) if mandatory_arg_number is None: break p_number = len(parameter) if p_number != mandatory_arg_number: raise InvalidRule( f"Condition `{condition.value}` requires {mandatory_arg_number} " f"arguments, got {p_number}.") else: break elif isinstance(parameter, _type): break else: human_type = _type.__name__ if _type is not None else "No parameter." raise InvalidRule( f"Invalid parameter type for condition `{condition.value}`. Expected: `{human_type}`" ) if author: try: await CONDITIONS_SANITY_CHECK[condition]( cog=cog, author=author, condition=condition, parameter=parameter) # type: ignore except KeyError: pass for raw_condition in self.conditions: condition = parameter = None if not isinstance(raw_condition, dict): raise InvalidRule( f"Invalid condition: `{raw_condition}`. Expected map.") for r, p in raw_condition.items(): condition, parameter = r, p if len(raw_condition) != 1: raise InvalidRule( f"Invalid format in the conditions. Make sure you've got the dashes right!" ) try: condition = Condition(condition) except ValueError: try: condition = ConditionBlock(condition) except ValueError: suggestion = make_fuzzy_suggestion( condition, [c.value for c in Condition]) raise InvalidRule( f"Invalid condition: `{condition}`.{suggestion}") if isinstance(condition, ConditionBlock): if parameter is None: raise InvalidRule("Condition blocks cannot be empty.") for p in parameter: await validate_condition(p) else: await validate_condition(raw_condition) # Basically a list of one-key dicts # We need to preserve order of actions for entry in self.actions: # This will be a single loop if not isinstance(entry, dict): raise InvalidRule(f"Invalid action: `{entry}`. Expected map.") if len(entry) != 1: raise InvalidRule( f"Invalid format in the actions. Make sure you've got the dashes right!" ) _type = None for action, parameter in entry.items(): try: action = Action(action) except ValueError: suggestion = make_fuzzy_suggestion( action, [a.value for a in Action]) raise InvalidRule( f"Invalid action: `{action}`.{suggestion}") if not is_action_allowed_in_events(action): raise InvalidRule( f"Action `{action.value}` not allowed in the event(s) you have defined." ) for _type in ACTIONS_PARAM_TYPE[action]: if _type is None and parameter is None: break elif _type is None: continue if _type == list and isinstance(parameter, list): mandatory_arg_number = ACTIONS_ARGS_N.get(action, None) if mandatory_arg_number is None: break p_number = len(parameter) if p_number != mandatory_arg_number: raise InvalidRule( f"Action `{action.value}` requires {mandatory_arg_number} " f"arguments, got {p_number}.") else: break elif isinstance(parameter, _type): break else: human_type = _type.__name__ if _type is not None else "No parameter." raise InvalidRule( f"Invalid parameter type for action `{action.value}`. Expected: `{human_type}`" ) if author: try: await ACTIONS_SANITY_CHECK[action](cog=cog, author=author, action=action, parameter=parameter) except KeyError: pass
async def do_actions(self, *, cog, user: discord.Member = None, message: discord.Message = None, guild: discord.Guild = None): if message and not user: user = message.author guild = guild if guild else user.guild channel: discord.Channel = message.channel if message else None templates_vars = { "rule_name": self.name, "guild": str(guild), "guild_id": guild.id } if user: templates_vars.update({ "user": str(user), "user_name": user.name, "user_id": user.id, "user_mention": user.mention, "user_nickname": str(user.nick), "user_created_at": user.created_at, "user_joined_at": user.joined_at, "user_heat": heat.get_user_heat(user), }) if message: templates_vars["message"] = message.content.replace("@", "@\u200b") templates_vars["message_clean"] = message.clean_content templates_vars["message_id"] = message.id templates_vars["message_created_at"] = message.created_at templates_vars["message_link"] = message.jump_url if message.attachments: attachment = message.attachments[0] templates_vars["attachment_filename"] = attachment.filename templates_vars["attachment_url"] = attachment.url if channel: templates_vars["channel"] = str(channel) templates_vars["channel_name"] = channel.name templates_vars["channel_id"] = channel.id templates_vars["channel_mention"] = channel.mention templates_vars[ "channel_category"] = channel.category.name if channel.category else "None" templates_vars[ "channel_category_id"] = channel.category.id if channel.category else "0" templates_vars["channel_heat"] = heat.get_channel_heat(channel) #for heat_key in heat.get_custom_heat_keys(guild): # templates_vars[f"custom_heat_{heat_key}"] = heat.get_custom_heat(guild, heat_key) last_sent_message: Optional[discord.Message] = None last_expel_action = None for entry in self.actions: for action, value in entry.items(): action = Action(action) self.last_action = action if action == Action.DmUser: text = Template(value).safe_substitute(templates_vars) try: last_sent_message = await user.send(text) except: cog.send_to_monitor( guild, f"[Warden] ({self.name}): Failed to DM user " f"{user} ({user.id})") last_sent_message = None elif action == Action.DeleteUserMessage: await message.delete() elif action == Action.NotifyStaff: text = Template(value).safe_substitute(templates_vars) last_sent_message = await cog.send_notification( guild, text, allow_everyone_ping=True) elif action == Action.NotifyStaffAndPing: text = Template(value).safe_substitute(templates_vars) last_sent_message = await cog.send_notification( guild, text, ping=True, allow_everyone_ping=True) elif action == Action.NotifyStaffWithEmbed: title, content = (value[0], value[1]) em = self._build_embed(title, content, templates_vars=templates_vars) last_sent_message = await cog.send_notification( guild, "", embed=em, allow_everyone_ping=True) elif action == Action.SendInChannel: text = Template(value).safe_substitute(templates_vars) last_sent_message = await channel.send( text, allowed_mentions=ALLOW_ALL_MENTIONS) elif action == Action.SetChannelSlowmode: timedelta = parse_timedelta(value) await channel.edit(slowmode_delay=timedelta.seconds) elif action == Action.Dm: _id_or_name, content = (value[0], value[1]) user_to_dm = guild.get_member(_id_or_name) if not user_to_dm: user_to_dm = discord.utils.get(guild.members, name=_id_or_name) if not user_to_dm: continue content = Template(content).safe_substitute(templates_vars) try: last_sent_message = await user_to_dm.send(content) except: cog.send_to_monitor( guild, f"[Warden] ({self.name}): Failed to DM user " f"{user_to_dm} ({user_to_dm.id})") last_sent_message = None elif action == Action.SendToChannel: _id_or_name, content = (value[0], value[1]) channel_dest = guild.get_channel(_id_or_name) if not channel_dest: channel_dest = discord.utils.get(guild.text_channels, name=_id_or_name) if not channel_dest: raise ExecutionError( f"Channel '{_id_or_name}' not found.") content = Template(content).safe_substitute(templates_vars) last_sent_message = await channel_dest.send( content, allowed_mentions=ALLOW_ALL_MENTIONS) elif action == Action.AddRolesToUser: to_assign = [] for role_id_or_name in value: role = guild.get_role(role_id_or_name) if role is None: role = discord.utils.get(guild.roles, name=role_id_or_name) if role: to_assign.append(role) to_assign = list(set(to_assign)) to_assign = [r for r in to_assign if r not in user.roles] if to_assign: await user.add_roles( *to_assign, reason=f"Assigned by Warden rule '{self.name}'") elif action == Action.RemoveRolesFromUser: to_unassign = [] for role_id_or_name in value: role = guild.get_role(role_id_or_name) if role is None: role = discord.utils.get(guild.roles, name=role_id_or_name) if role: to_unassign.append(role) to_unassign = list(set(to_unassign)) to_unassign = [r for r in to_unassign if r in user.roles] if to_unassign: await user.remove_roles( *to_unassign, reason=f"Unassigned by Warden rule '{self.name}'") elif action == Action.SetUserNickname: if value == "": value = None else: value = Template(value).safe_substitute(templates_vars) await user.edit( nick=value, reason=f"Changed nickname by Warden rule '{self.name}'" ) elif action == Action.BanAndDelete: if user not in guild.members: raise ExecutionError( f"User {user} ({user.id}) not in the server.") reason = f"Banned by Warden rule '{self.name}'" await guild.ban(user, delete_message_days=value, reason=reason) last_expel_action = ModAction.Ban cog.dispatch_event("member_remove", user, ModAction.Ban.value, reason) elif action == Action.Kick: if user not in guild.members: raise ExecutionError( f"User {user} ({user.id}) not in the server.") reason = f"Kicked by Warden action '{self.name}'" await guild.kick(user, reason=reason) last_expel_action = Action.Kick cog.dispatch_event("member_remove", user, ModAction.Kick.value, reason) elif action == Action.Softban: if user not in guild.members: raise ExecutionError( f"User {user} ({user.id}) not in the server.") reason = f"Softbanned by Warden rule '{self.name}'" await guild.ban(user, delete_message_days=1, reason=reason) await guild.unban(user) last_expel_action = Action.Softban cog.dispatch_event("member_remove", user, ModAction.Softban.value, reason) elif action == Action.Modlog: if last_expel_action is None: continue reason = Template(value).safe_substitute(templates_vars) await modlog.create_case( cog.bot, guild, utcnow(), last_expel_action.value, user, guild.me, reason, until=None, channel=None, ) elif action == Action.EnableEmergencyMode: if value: cog.emergency_mode[guild.id] = EmergencyMode( manual=True) else: try: del cog.emergency_mode[guild.id] except KeyError: pass elif action == Action.SendToMonitor: value = Template(value).safe_substitute(templates_vars) cog.send_to_monitor(guild, f"[Warden] ({self.name}): {value}") elif action == Action.AddUserHeatpoint: timedelta = parse_timedelta(value) heat.increase_user_heat(user, timedelta) # type: ignore templates_vars["user_heat"] = heat.get_user_heat(user) elif action == Action.AddUserHeatpoints: points_n = value[0] timedelta = parse_timedelta(value[1]) for _ in range(points_n): heat.increase_user_heat(user, timedelta) # type: ignore templates_vars["user_heat"] = heat.get_user_heat(user) elif action == Action.AddChannelHeatpoint: timedelta = parse_timedelta(value) heat.increase_channel_heat(channel, timedelta) # type: ignore templates_vars["channel_heat"] = heat.get_channel_heat( channel) elif action == Action.AddChannelHeatpoints: points_n = value[0] timedelta = parse_timedelta(value[1]) for _ in range(points_n): heat.increase_channel_heat(channel, timedelta) # type: ignore templates_vars["channel_heat"] = heat.get_channel_heat( channel) elif action == Action.AddCustomHeatpoint: heat_key = Template( value[0]).safe_substitute(templates_vars) timedelta = parse_timedelta(value[1]) heat.increase_custom_heat(guild, heat_key, timedelta) # type: ignore #templates_vars[f"custom_heat_{heat_key}"] = heat.get_custom_heat(guild, heat_key) elif action == Action.AddCustomHeatpoints: heat_key = Template( value[0]).safe_substitute(templates_vars) points_n = value[1] timedelta = parse_timedelta(value[2]) for _ in range(points_n): heat.increase_custom_heat(guild, heat_key, timedelta) # type: ignore #templates_vars[f"custom_heat_{heat_key}"] = heat.get_custom_heat(guild, heat_key) elif action == Action.EmptyUserHeat: heat.empty_user_heat(user) elif action == Action.EmptyChannelHeat: heat.empty_channel_heat(channel) elif action == Action.EmptyCustomHeat: heat_key = Template(value).safe_substitute(templates_vars) heat.empty_custom_heat(guild, heat_key) elif action == Action.IssueCommand: issuer = guild.get_member(value[0]) if issuer is None: raise ExecutionError( f"User {value[0]} is not in the server.") msg_obj = df_cache.get_msg_obj() if msg_obj is None: raise ExecutionError( f"Failed to issue command. Sorry!") if message is None: notify_channel_id = await cog.config.guild( guild).notify_channel() msg_obj.channel = guild.get_channel(notify_channel_id) if msg_obj.channel is None: raise ExecutionError( f"Failed to issue command. Sorry!") else: msg_obj.channel = message.channel msg_obj.author = issuer prefix = await cog.bot.get_prefix(msg_obj) msg_obj.content = prefix[0] + Template(str( value[1])).safe_substitute(templates_vars) cog.bot.dispatch("message", msg_obj) elif action == Action.DeleteLastMessageSentAfter: if last_sent_message is not None: timedelta = parse_timedelta(value) cog.loop.create_task( delete_message_after(last_sent_message, timedelta.seconds)) last_sent_message = None elif action == Action.NoOp: pass else: raise ExecutionError(f"Unhandled action '{self.name}'.") return bool(last_expel_action)