1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Prefix and slash agnostic utilities for dispatching incoming events onto framework commands

use crate::serenity_prelude as serenity;

/// Retrieves user permissions in the given channel. If unknown, returns None. If in DMs, returns
/// `Permissions::all()`.
async fn user_permissions(
    ctx: &serenity::Context,
    guild_id: Option<serenity::GuildId>,
    channel_id: serenity::ChannelId,
    user_id: serenity::UserId,
) -> Option<serenity::Permissions> {
    let guild_id = match guild_id {
        Some(x) => x,
        None => return Some(serenity::Permissions::all()), // no permission checks in DMs
    };

    let guild = guild_id.to_partial_guild(ctx).await.ok()?;

    // Use to_channel so that it can fallback on HTTP for threads (which aren't in cache usually)
    let channel = match channel_id.to_channel(ctx).await {
        Ok(serenity::Channel::Guild(channel)) => channel,
        Ok(_other_channel) => {
            log::warn!(
                "guild message was supposedly sent in a non-guild channel. Denying invocation"
            );
            return None;
        }
        Err(_) => return None,
    };

    let member = guild.member(ctx, user_id).await.ok()?;

    guild.user_permissions_in(&channel, &member).ok()
}

/// Retrieves the set of permissions that are lacking, relative to the given required permission set
///
/// Returns None if permissions couldn't be retrieved
async fn missing_permissions<U, E>(
    ctx: crate::Context<'_, U, E>,
    user: serenity::UserId,
    required_permissions: serenity::Permissions,
) -> Option<serenity::Permissions> {
    if required_permissions.is_empty() {
        return Some(serenity::Permissions::empty());
    }

    let permissions = user_permissions(
        ctx.serenity_context(),
        ctx.guild_id(),
        ctx.channel_id(),
        user,
    )
    .await;
    Some(required_permissions - permissions?)
}

/// See [`check_permissions_and_cooldown`]. Runs the check only for a single command. The caller
/// should call this multiple time for each parent command to achieve the check inheritance logic.
async fn check_permissions_and_cooldown_single<'a, U, E>(
    ctx: crate::Context<'a, U, E>,
    cmd: &'a crate::Command<U, E>,
) -> Result<(), crate::FrameworkError<'a, U, E>> {
    // Skip command checks if `FrameworkOptions::skip_checks_for_owners` is set to true
    if ctx.framework().options.skip_checks_for_owners
        && ctx.framework().options().owners.contains(&ctx.author().id)
    {
        return Ok(());
    }

    if cmd.owners_only && !ctx.framework().options().owners.contains(&ctx.author().id) {
        return Err(crate::FrameworkError::NotAnOwner { ctx });
    }

    if cmd.guild_only {
        match ctx.guild_id() {
            None => return Err(crate::FrameworkError::GuildOnly { ctx }),
            Some(guild_id) => {
                #[cfg(feature = "cache")]
                if ctx.framework().options().require_cache_for_guild_check
                    && ctx.cache().guild_field(guild_id, |_| ()).is_none()
                {
                    return Err(crate::FrameworkError::GuildOnly { ctx });
                }
                #[cfg(not(feature = "cache"))]
                let _ = guild_id;
            }
        }
    }

    if cmd.dm_only && ctx.guild_id().is_some() {
        return Err(crate::FrameworkError::DmOnly { ctx });
    }

    if cmd.nsfw_only {
        let channel = match ctx.channel_id().to_channel(ctx.serenity_context()).await {
            Ok(channel) => channel,
            Err(e) => {
                log::warn!("Error when getting channel: {}", e);

                return Err(crate::FrameworkError::NsfwOnly { ctx });
            }
        };

        if !channel.is_nsfw() {
            return Err(crate::FrameworkError::NsfwOnly { ctx });
        }
    }

    // Make sure that user has required permissions
    match missing_permissions(ctx, ctx.author().id, cmd.required_permissions).await {
        Some(missing_permissions) if missing_permissions.is_empty() => {}
        Some(missing_permissions) => {
            return Err(crate::FrameworkError::MissingUserPermissions {
                ctx,
                missing_permissions: Some(missing_permissions),
            })
        }
        // Better safe than sorry: when perms are unknown, restrict access
        None => {
            return Err(crate::FrameworkError::MissingUserPermissions {
                ctx,
                missing_permissions: None,
            })
        }
    }

    // Before running any pre-command checks, make sure the bot has the permissions it needs
    match missing_permissions(ctx, ctx.framework().bot_id, cmd.required_bot_permissions).await {
        Some(missing_permissions) if missing_permissions.is_empty() => {}
        Some(missing_permissions) => {
            return Err(crate::FrameworkError::MissingBotPermissions {
                ctx,
                missing_permissions,
            })
        }
        // When in doubt, just let it run. Not getting fancy missing permissions errors is better
        // than the command not executing at all
        None => {}
    }

    // Only continue if command checks returns true
    // First perform global checks, then command checks (if necessary)
    for check in Option::iter(&ctx.framework().options().command_check).chain(&cmd.checks) {
        match check(ctx).await {
            Ok(true) => {}
            Ok(false) => {
                return Err(crate::FrameworkError::CommandCheckFailed { ctx, error: None })
            }
            Err(error) => {
                return Err(crate::FrameworkError::CommandCheckFailed {
                    error: Some(error),
                    ctx,
                })
            }
        }
    }

    if !ctx.framework().options().manual_cooldowns {
        let cooldowns = &cmd.cooldowns;
        let remaining_cooldown = cooldowns.lock().unwrap().remaining_cooldown(ctx);
        if let Some(remaining_cooldown) = remaining_cooldown {
            return Err(crate::FrameworkError::CooldownHit {
                ctx,
                remaining_cooldown,
            });
        }
    }

    Ok(())
}

/// Checks if the invoker is allowed to execute this command at this point in time
///
/// Doesn't actually start the cooldown timer! This should be done by the caller later, after
/// argument parsing.
/// (A command that didn't even get past argument parsing shouldn't trigger cooldowns)
#[allow(clippy::needless_lifetimes)] // false positive (clippy issue 7271)
pub async fn check_permissions_and_cooldown<'a, U, E>(
    ctx: crate::Context<'a, U, E>,
) -> Result<(), crate::FrameworkError<'a, U, E>> {
    for parent_command in ctx.parent_commands() {
        check_permissions_and_cooldown_single(ctx, parent_command).await?;
    }
    check_permissions_and_cooldown_single(ctx, ctx.command()).await?;

    Ok(())
}