reaction_roles.rs 7.18 KB
Newer Older
1
use crate::config::{ReactRoleMap, CONFIG};
tec's avatar
tec committed
2
use crate::util::{get_react_from_string, get_string_from_react};
Timothy du Heaume's avatar
Timothy du Heaume committed
3
use rayon::prelude::*;
4
use serenity::{
tec's avatar
tec committed
5
    client::Context,
6
    model::{channel::Message, channel::Reaction, id::RoleId, id::UserId},
7
};
tec's avatar
tec committed
8 9
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
tec's avatar
tec committed
10

tec's avatar
tec committed
11
pub fn add_role_by_reaction(ctx: &Context, msg: Message, added_reaction: Reaction) {
12 13
    let user = added_reaction
        .user_id
tec's avatar
tec committed
14
        .to_user(ctx)
15 16
        .expect("Unable to get user");
    if let Some(role_id) = CONFIG
tec's avatar
tec committed
17 18 19 20
        .react_role_messages
        .iter()
        .find(|rrm| rrm.message == msg.id)
        .and_then(|reaction_mapping| {
21
            let react_as_string = get_string_from_react(&added_reaction.emoji);
Timothy du Heaume's avatar
Timothy du Heaume committed
22
            reaction_mapping.mapping.get(&react_as_string)
tec's avatar
tec committed
23
        })
24 25 26 27 28
    {
        info!(
            "{} requested role '{}'",
            user.name,
            role_id
tec's avatar
tec committed
29
                .to_role_cached(ctx)
30 31 32 33 34 35 36 37 38 39 40 41
                .expect("Unable to get role")
                .name
        );
        ctx.http
            .add_member_role(
                CONFIG.server_id,
                added_reaction.user_id.0,
                *role_id.as_u64(),
            )
            .ok();
    } else {
        warn!("{} provided invalid react for role", user.name);
tec's avatar
tec committed
42
        e!("Unable to delete react: {:?}", added_reaction.delete(ctx));
43
    }
tec's avatar
tec committed
44 45
}

tec's avatar
tec committed
46
pub fn remove_role_by_reaction(ctx: &Context, msg: Message, removed_reaction: Reaction) {
tec's avatar
tec committed
47 48 49 50 51
    CONFIG
        .react_role_messages
        .iter()
        .find(|rrm| rrm.message == msg.id)
        .and_then(|reaction_mapping| {
52
            let react_as_string = get_string_from_react(&removed_reaction.emoji);
Timothy du Heaume's avatar
Timothy du Heaume committed
53
            reaction_mapping.mapping.get(&react_as_string)
tec's avatar
tec committed
54 55
        })
        .and_then(|role_id| {
56 57 58 59
            info!(
                "{} requested removal of role '{}'",
                msg.author.name,
                role_id
tec's avatar
tec committed
60
                    .to_role_cached(ctx)
61 62 63
                    .expect("Unable to get role")
                    .name
            );
Timothy du Heaume's avatar
Timothy du Heaume committed
64
            ctx.http
65 66 67 68 69
                .remove_member_role(
                    CONFIG.server_id,
                    removed_reaction.user_id.0,
                    *role_id.as_u64(),
                )
Timothy du Heaume's avatar
Timothy du Heaume committed
70
                .ok()
tec's avatar
tec committed
71
        });
tec's avatar
tec committed
72
}
73

tec's avatar
tec committed
74
pub fn sync_all_role_reactions(ctx: &Context) {
75
    info!("Syncing roles to reactions");
tec's avatar
tec committed
76
    let messages_with_role_mappings = get_all_role_reaction_message(ctx);
77
    info!("  Sync: reaction messages fetched");
78
    let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
79
    info!("  Sync: guild fetched");
80 81 82
    // this method supports paging, but we probably don't need it since the server only has a couple of
    // hundred members. the Reaction.users() method can apparently only retrieve 100 users at once, but
    // this one seems to work fine when set to 1000 (I tried 10,000 but the api returned a 400)
83
    let mut all_members = ctx
tec's avatar
tec committed
84 85 86
        .http
        .get_guild_members(CONFIG.server_id, Some(1000), None)
        .unwrap();
87
    all_members.retain(|m| m.user_id() != CONFIG.bot_id);
88
    info!("  Sync: all members fetched");
89

90 91 92 93
    let mut roles_to_add: HashMap<UserId, Vec<RoleId>> =
        HashMap::from_iter(all_members.iter().map(|m| (m.user_id(), Vec::new())));
    let mut roles_to_remove: HashMap<UserId, Vec<RoleId>> =
        HashMap::from_iter(all_members.iter().map(|m| (m.user_id(), Vec::new())));
94

Ash's avatar
Ash committed
95
    for (i, (message, mapping)) in messages_with_role_mappings.iter().enumerate() {
96
        info!("  Sync: prossessing message #{}", i);
97 98 99
        for react in &message.reactions {
            let react_as_string = get_string_from_react(&react.reaction_type);
            if mapping.contains_key(&react_as_string) {
Ash's avatar
cleanup  
Ash committed
100
                continue
101 102 103 104 105 106
            }
            info!(
                "    message #{}: Removing non-role react '{}'",
                i, react_as_string
            );
            for _illegal_react in
tec's avatar
tec committed
107
                &message.reaction_users(ctx, react.reaction_type.clone(), Some(100), None)
108 109 110 111
            {
                warn!("    need to implement react removal");
            }
        }
Ash's avatar
Ash committed
112
        for (react, role) in *mapping {
113
            info!("    message #{}: processing react '{}'", i, react);
114 115
            // TODO: proper pagination for the unlikely scenario that there are more than 100 (255?) reactions?
            let reaction_type = get_react_from_string(react.clone(), guild.clone());
tec's avatar
tec committed
116
            let reactors = message
117
                .reaction_users(ctx.http.clone(), reaction_type.clone(), Some(100), None)
tec's avatar
tec committed
118
                .unwrap();
119 120
            let reactor_ids: HashSet<UserId> = HashSet::from_iter(reactors.iter().map(|r| r.id));

121 122 123 124
            // ensure bot has reacted
            if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) {
                e!(
                    "Unable to add reaction, {:?}",
tec's avatar
tec committed
125
                    message.react(ctx, reaction_type)
126 127 128
                );
            }

129 130 131 132 133 134 135
            for member in all_members.clone() {
                let user_id = &member.user_id();
                if reactor_ids.contains(&user_id) {
                    if !member.roles.iter().any(|r| r == role) {
                        roles_to_add.get_mut(&user_id).unwrap().push(*role);
                    }
                } else if member.roles.iter().any(|r| r == role) {
136
                    roles_to_remove.get_mut(&user_id).unwrap().push(*role);
137 138 139 140
                }
            }
        }
    }
141
    info!("  Sync: finished determing roles to add/remove");
142 143 144

    for (user_id, roles) in roles_to_add {
        if !roles.is_empty() {
145 146 147 148 149
            let mut member = all_members
                .iter()
                .find(|m| m.user_id() == user_id)
                .unwrap()
                .clone();
150 151 152
            member.add_roles(ctx.http.clone(), &roles[..]).unwrap();
        }
    }
153
    info!("  Sync: (any) missing roles added");
154 155
    for (user_id, roles) in roles_to_remove {
        if !roles.is_empty() {
156 157 158 159 160
            let mut member = all_members
                .iter()
                .find(|m| m.user_id() == user_id)
                .unwrap()
                .clone();
161 162 163
            member.remove_roles(ctx.http.clone(), &roles[..]).unwrap();
        }
    }
164 165
    info!("  Sync: (any) superflous roles removed");
    info!("Role reaction sync complete");
166 167
}

Timothy du Heaume's avatar
Timothy du Heaume committed
168
fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactRoleMap)> {
169
    let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
170
    info!("  Find role-react message: guild determined");
171
    let channels = ctx.http.get_channels(*guild.id.as_u64()).unwrap();
172
    info!("  Find role-react message: channels determined");
Timothy du Heaume's avatar
Timothy du Heaume committed
173
    let http = ctx.http.clone();
Timothy du Heaume's avatar
Timothy du Heaume committed
174
    channels
Timothy du Heaume's avatar
Timothy du Heaume committed
175
        .par_iter()
tec's avatar
tec committed
176
        .flat_map(|channel| {
Timothy du Heaume's avatar
Timothy du Heaume committed
177 178 179
            // since we don't know which channels the messages are in, we check every combination
            // of message and channel and ignore the bad matches using .ok() and .filter_map()
            let h = http.clone(); // thread-local copy
180 181 182 183 184 185 186 187
            CONFIG
                .react_role_messages
                .par_iter()
                .filter_map(move |rrm| {
                    h.get_message(*channel.id.as_u64(), *rrm.message.as_u64())
                        .ok()
                        .map(|m| (m, &rrm.mapping))
                })
tec's avatar
tec committed
188
        })
Timothy du Heaume's avatar
Timothy du Heaume committed
189
        .collect()
190
}