Beispiel #1
0
    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),
        )
Beispiel #2
0
 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
Beispiel #3
0
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`")
Beispiel #4
0
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`")
Beispiel #5
0
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`")
Beispiel #6
0
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.")
Beispiel #7
0
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`")
Beispiel #8
0
    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]
Beispiel #9
0
    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())
Beispiel #11
0
    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)
Beispiel #12
0
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)
Beispiel #13
0
    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
Beispiel #14
0
    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)