zynko 0.1.0

Biblioteca oficial para bots do Zynko: API, gateway e voz.
Documentation
//! What someone may do in a channel, worked out the server's way.

use crate::bits::Permissions;
use crate::cache::CachedGuild;
use crate::types::Channel;

/// What someone may do in a channel: @everyone and their roles, then the channel's overwrites.
/// Owners and admins may do everything. 0 when the user is not a member or the channel is not
/// in `guild`.
pub fn compute_permissions(guild: &CachedGuild, channel: &Channel, user_id: &str) -> u64 {
    const ALL: u64 = Permissions::ALL;
    if channel.guild_id.as_deref() != Some(guild.id.as_str()) {
        return 0;
    }
    let Some(member) = guild.members.get(user_id) else {
        return 0;
    };
    if guild.owner_id == user_id || member.is_admin {
        return ALL;
    }
    let mut base = guild.roles.get(&guild.id).map_or(0, |r| r.permissions);
    for r in &member.roles {
        base |= guild.roles.get(r).map_or(0, |r| r.permissions);
    }
    if base & Permissions::ADMINISTRATOR != 0 {
        return ALL;
    }
    let overwrites = channel.permission_overwrites.as_deref().unwrap_or(&[]);
    let mut p = base;
    if let Some(everyone) = overwrites.iter().find(|o| o.kind == 0 && o.id == guild.id) {
        p = (p & !everyone.deny) | everyone.allow;
    }
    let (mut allow, mut deny) = (0u64, 0u64);
    for o in overwrites {
        if o.kind == 0 && o.id != guild.id && member.roles.contains(&o.id) {
            allow |= o.allow;
            deny |= o.deny;
        }
    }
    p = (p & !deny) | allow;
    if let Some(own) = overwrites.iter().find(|o| o.kind == 1 && o.id == user_id) {
        p = (p & !own.deny) | own.allow;
    }
    if p & Permissions::VIEW_CHANNEL == 0 {
        return 0;
    }
    if p & Permissions::SEND_MESSAGES == 0 {
        p &= !Permissions::ATTACH_FILES;
    }
    if p & Permissions::CONNECT == 0 {
        p &= !(Permissions::SPEAK | Permissions::STREAM);
    }
    p
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{Member, Overwrite, Role, User};
    use Permissions as P;

    const G: &str = "100";
    const MOD: &str = "200";
    const MUTED: &str = "201";

    fn guild(everyone: u64) -> CachedGuild {
        let mut g = CachedGuild {
            id: G.into(),
            owner_id: "1".into(),
            ..Default::default()
        };
        for (id, perms) in [
            (G, everyone),
            (MOD, P::KICK_MEMBERS | P::MANAGE_MESSAGES),
            (MUTED, 0),
        ] {
            g.roles.insert(
                id.into(),
                Role {
                    id: id.into(),
                    guild_id: G.into(),
                    permissions: perms,
                    ..Default::default()
                },
            );
        }
        for (uid, roles, admin) in [
            ("1", vec![], false),
            ("2", vec![], false),
            ("3", vec![MOD], false),
            ("4", vec![MUTED], false),
            ("5", vec![], true),
        ] {
            g.members.insert(
                uid.into(),
                Member {
                    guild_id: G.into(),
                    user: User {
                        id: uid.into(),
                        ..Default::default()
                    },
                    is_admin: admin,
                    roles: roles.into_iter().map(String::from).collect(),
                    ..Default::default()
                },
            );
        }
        g
    }

    fn channel(overwrites: Vec<Overwrite>) -> Channel {
        Channel {
            id: "300".into(),
            guild_id: Some(G.into()),
            permission_overwrites: Some(overwrites),
            ..Default::default()
        }
    }

    fn ow(id: &str, kind: u8, allow: u64, deny: u64) -> Overwrite {
        Overwrite {
            id: id.into(),
            kind,
            allow,
            deny,
        }
    }

    const BASIC: u64 = P::VIEW_CHANNEL
        | P::SEND_MESSAGES
        | P::ATTACH_FILES
        | P::CONNECT
        | P::SPEAK
        | P::READ_MESSAGE_HISTORY;

    #[test]
    fn owner_and_admins_get_everything() {
        let g = guild(BASIC);
        let c = channel(vec![ow(G, 0, 0, P::VIEW_CHANNEL)]);
        assert_eq!(compute_permissions(&g, &c, "1"), P::ALL);
        assert_eq!(compute_permissions(&g, &c, "5"), P::ALL);
    }

    #[test]
    fn administrator_role_bit_gives_everything() {
        let mut g = guild(BASIC);
        g.roles.get_mut(MOD).unwrap().permissions |= P::ADMINISTRATOR;
        assert_eq!(compute_permissions(&g, &channel(vec![]), "3"), P::ALL);
    }

    #[test]
    fn roles_add_to_everyone() {
        let g = guild(BASIC);
        let c = channel(vec![]);
        assert_eq!(compute_permissions(&g, &c, "2"), BASIC);
        assert_eq!(
            compute_permissions(&g, &c, "3"),
            BASIC | P::KICK_MEMBERS | P::MANAGE_MESSAGES
        );
    }

    #[test]
    fn non_members_and_foreign_channels_get_nothing() {
        let g = guild(BASIC);
        assert_eq!(compute_permissions(&g, &channel(vec![]), "999"), 0);
        let mut other = channel(vec![]);
        other.guild_id = Some("555".into());
        assert_eq!(compute_permissions(&g, &other, "2"), 0);
    }

    #[test]
    fn overwrites_apply_everyone_then_roles_then_member() {
        let g = guild(BASIC);
        // @everyone cannot send; the muted role cannot speak; the mod role may send again;
        // member 2 is allowed to send on its own.
        let c = channel(vec![
            ow(G, 0, 0, P::SEND_MESSAGES),
            ow(MUTED, 0, 0, P::SPEAK),
            ow(MOD, 0, P::SEND_MESSAGES, 0),
            ow("2", 1, P::SEND_MESSAGES, 0),
        ]);
        // No SEND_MESSAGES also drops ATTACH_FILES.
        assert_eq!(
            compute_permissions(&g, &c, "4"),
            BASIC & !(P::SEND_MESSAGES | P::ATTACH_FILES | P::SPEAK)
        );
        assert_eq!(
            compute_permissions(&g, &c, "3"),
            BASIC | P::KICK_MEMBERS | P::MANAGE_MESSAGES
        );
        assert_eq!(compute_permissions(&g, &c, "2"), BASIC);
    }

    #[test]
    fn role_allow_wins_over_role_deny() {
        let mut g = guild(BASIC);
        g.members.get_mut("4").unwrap().roles.push(MOD.into());
        let c = channel(vec![
            ow(MUTED, 0, 0, P::SEND_MESSAGES),
            ow(MOD, 0, P::SEND_MESSAGES, 0),
        ]);
        assert!(compute_permissions(&g, &c, "4") & P::SEND_MESSAGES != 0);
    }

    #[test]
    fn member_overwrite_beats_roles() {
        let g = guild(BASIC);
        let c = channel(vec![
            ow(MOD, 0, P::MANAGE_MESSAGES, 0),
            ow("3", 1, 0, P::MANAGE_MESSAGES | P::SEND_MESSAGES),
        ]);
        let p = compute_permissions(&g, &c, "3");
        assert_eq!(p & P::MANAGE_MESSAGES, 0);
        assert_eq!(p & P::ATTACH_FILES, 0);
    }

    #[test]
    fn no_view_means_nothing() {
        let g = guild(BASIC);
        let c = channel(vec![ow(G, 0, 0, P::VIEW_CHANNEL)]);
        assert_eq!(compute_permissions(&g, &c, "2"), 0);
        let c = channel(vec![
            ow(G, 0, 0, P::VIEW_CHANNEL),
            ow("2", 1, P::VIEW_CHANNEL, 0),
        ]);
        assert_eq!(compute_permissions(&g, &c, "2"), BASIC);
    }

    #[test]
    fn no_connect_drops_speak_and_stream() {
        let g = guild(BASIC | P::STREAM);
        let c = channel(vec![ow(G, 0, 0, P::CONNECT)]);
        let p = compute_permissions(&g, &c, "2");
        assert_eq!(p & (P::CONNECT | P::SPEAK | P::STREAM), 0);
        assert!(p & P::VIEW_CHANNEL != 0);
    }
}