reaction_roles.rs 7.36 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

11
12
13
14
15
16
17
18
19
macro_rules! e {
    ($error: literal, $x:expr) => {
        match $x {
            Ok(_) => (),
            Err(why) => error!($error, why),
        }
    };
}

20
pub fn add_role_by_reaction(ctx: Context, msg: Message, added_reaction: Reaction) {
21
22
23
24
25
    let user = added_reaction
        .user_id
        .to_user(&ctx)
        .expect("Unable to get user");
    if let Some(role_id) = CONFIG
tec's avatar
tec committed
26
27
28
29
        .react_role_messages
        .iter()
        .find(|rrm| rrm.message == msg.id)
        .and_then(|reaction_mapping| {
30
            let react_as_string = get_string_from_react(&added_reaction.emoji);
Timothy du Heaume's avatar
Timothy du Heaume committed
31
            reaction_mapping.mapping.get(&react_as_string)
tec's avatar
tec committed
32
        })
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
    {
        info!(
            "{} requested role '{}'",
            user.name,
            role_id
                .to_role_cached(&ctx)
                .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);
        e!("Unable to delete react: {:?}", added_reaction.delete(&ctx));
    }
tec's avatar
tec committed
53
54
}

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

83
pub fn sync_all_role_reactions(ctx: Context) {
84
    info!("Syncing roles to reactions");
85
    let messages_with_role_mappings = get_all_role_reaction_message(&ctx);
86
    info!("  Sync: reaction messages fetched");
87
    let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
88
    info!("  Sync: guild fetched");
89
90
91
    // 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)
92
    let mut all_members = ctx
tec's avatar
tec committed
93
94
95
        .http
        .get_guild_members(CONFIG.server_id, Some(1000), None)
        .unwrap();
96
    all_members.retain(|m| m.user_id() != CONFIG.bot_id);
97
    info!("  Sync: all members fetched");
98

99
100
101
102
    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())));
103

104
    let mut i = 0;
105
    for (message, mapping) in messages_with_role_mappings {
106
107
        i += 1;
        info!("  Sync: prossessing message #{}", i);
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
        for react in &message.reactions {
            let react_as_string = get_string_from_react(&react.reaction_type);
            if mapping.contains_key(&react_as_string) {
                continue;
            }
            info!(
                "    message #{}: Removing non-role react '{}'",
                i, react_as_string
            );
            for _illegal_react in
                &message.reaction_users(&ctx, react.reaction_type.clone(), Some(100), None)
            {
                warn!("    need to implement react removal");
            }
        }
123
        for (react, role) in mapping {
124
            info!("    message #{}: processing react '{}'", i, react);
125
126
            // 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
127
            let reactors = message
128
                .reaction_users(ctx.http.clone(), reaction_type.clone(), Some(100), None)
tec's avatar
tec committed
129
                .unwrap();
130
131
            let reactor_ids: HashSet<UserId> = HashSet::from_iter(reactors.iter().map(|r| r.id));

132
133
134
135
136
137
138
139
            // ensure bot has reacted
            if !reactor_ids.contains(&UserId::from(CONFIG.bot_id)) {
                e!(
                    "Unable to add reaction, {:?}",
                    message.react(&ctx, reaction_type)
                );
            }

140
141
142
143
144
145
146
            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) {
147
                    roles_to_remove.get_mut(&user_id).unwrap().push(*role);
148
149
150
151
                }
            }
        }
    }
152
    info!("  Sync: finished determing roles to add/remove");
153
154
155

    for (user_id, roles) in roles_to_add {
        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.add_roles(ctx.http.clone(), &roles[..]).unwrap();
        }
    }
164
    info!("  Sync: (any) missing roles added");
165
166
    for (user_id, roles) in roles_to_remove {
        if !roles.is_empty() {
167
168
169
170
171
            let mut member = all_members
                .iter()
                .find(|m| m.user_id() == user_id)
                .unwrap()
                .clone();
172
173
174
            member.remove_roles(ctx.http.clone(), &roles[..]).unwrap();
        }
    }
175
176
    info!("  Sync: (any) superflous roles removed");
    info!("Role reaction sync complete");
177
178
}

Timothy du Heaume's avatar
Timothy du Heaume committed
179
fn get_all_role_reaction_message(ctx: &Context) -> Vec<(Message, &'static ReactRoleMap)> {
180
    let guild = ctx.http.get_guild(CONFIG.server_id).unwrap();
181
    info!("  Find role-react message: guild determined");
182
    let channels = ctx.http.get_channels(*guild.id.as_u64()).unwrap();
183
    info!("  Find role-react message: channels determined");
Timothy du Heaume's avatar
Timothy du Heaume committed
184
    let http = ctx.http.clone();
Timothy du Heaume's avatar
Timothy du Heaume committed
185
    channels
Timothy du Heaume's avatar
Timothy du Heaume committed
186
        .par_iter()
tec's avatar
tec committed
187
        .flat_map(|channel| {
Timothy du Heaume's avatar
Timothy du Heaume committed
188
189
190
            // 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
191
192
193
194
195
196
197
198
            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
199
        })
Timothy du Heaume's avatar
Timothy du Heaume committed
200
        .collect()
201
}