1use std::borrow::Cow;
2
3use crate::cache::Cache;
4use crate::model::id::GuildId;
5use crate::model::mention::Mention;
6use crate::model::user::User;
7
8#[derive(Clone, Debug)]
10pub struct ContentSafeOptions {
11 clean_role: bool,
12 clean_user: bool,
13 clean_channel: bool,
14 clean_here: bool,
15 clean_everyone: bool,
16 show_discriminator: bool,
17 guild_reference: Option<GuildId>,
18}
19
20impl ContentSafeOptions {
21 #[must_use]
22 pub fn new() -> Self {
23 ContentSafeOptions::default()
24 }
25
26 #[must_use]
29 pub fn clean_role(mut self, b: bool) -> Self {
30 self.clean_role = b;
31
32 self
33 }
34
35 #[must_use]
39 pub fn clean_user(mut self, b: bool) -> Self {
40 self.clean_user = b;
41
42 self
43 }
44
45 #[must_use]
49 pub fn clean_channel(mut self, b: bool) -> Self {
50 self.clean_channel = b;
51
52 self
53 }
54
55 #[must_use]
61 pub fn show_discriminator(mut self, b: bool) -> Self {
62 self.show_discriminator = b;
63
64 self
65 }
66
67 #[must_use]
70 pub fn display_as_member_from<G: Into<GuildId>>(mut self, guild: G) -> Self {
71 self.guild_reference = Some(guild.into());
72
73 self
74 }
75
76 #[must_use]
78 pub fn clean_here(mut self, b: bool) -> Self {
79 self.clean_here = b;
80
81 self
82 }
83
84 #[must_use]
86 pub fn clean_everyone(mut self, b: bool) -> Self {
87 self.clean_everyone = b;
88
89 self
90 }
91}
92
93impl Default for ContentSafeOptions {
94 fn default() -> Self {
96 ContentSafeOptions {
97 clean_role: true,
98 clean_user: true,
99 clean_channel: true,
100 clean_here: true,
101 clean_everyone: true,
102 show_discriminator: true,
103 guild_reference: None,
104 }
105 }
106}
107
108pub fn content_safe(
142 cache: impl AsRef<Cache>,
143 s: impl AsRef<str>,
144 options: &ContentSafeOptions,
145 users: &[User],
146) -> String {
147 let mut content = clean_mentions(&cache, s, options, users);
148
149 if options.clean_here {
150 content = content.replace("@here", "@\u{200B}here");
151 }
152
153 if options.clean_everyone {
154 content = content.replace("@everyone", "@\u{200B}everyone");
155 }
156
157 content
158}
159
160fn clean_mentions(
161 cache: impl AsRef<Cache>,
162 s: impl AsRef<str>,
163 options: &ContentSafeOptions,
164 users: &[User],
165) -> String {
166 let s = s.as_ref();
167 let mut content = String::with_capacity(s.len());
168 let mut brackets = s.match_indices(['<', '>']).peekable();
169 let mut progress = 0;
170 while let Some((idx1, b1)) = brackets.next() {
171 if b1 == "<" {
173 if let Some(&(idx2, b2)) = brackets.peek() {
174 if b2 == ">" {
175 content.push_str(&s[progress..idx1]);
176 let mention_str = &s[idx1..=idx2];
177
178 let mut chars = mention_str.chars();
181 chars.next();
182 let should_parse = match chars.next() {
183 Some('#') => options.clean_channel,
184 Some('@') => {
185 if let Some('&') = chars.next() {
186 options.clean_role
187 } else {
188 options.clean_user
189 }
190 },
191 _ => false,
192 };
193
194 let mut cleaned = false;
196 if should_parse {
197 if let Ok(mention) = mention_str.parse() {
200 content.push_str(&clean_mention(&cache, mention, options, users));
201 cleaned = true;
202 }
203 }
204 if !cleaned {
205 content.push_str(mention_str);
206 }
207 progress = idx2 + 1;
208 }
209 }
210 }
211 }
212 content.push_str(&s[progress..]);
213 content
214}
215
216fn clean_mention(
217 cache: impl AsRef<Cache>,
218 mention: Mention,
219 options: &ContentSafeOptions,
220 users: &[User],
221) -> Cow<'static, str> {
222 let cache = cache.as_ref();
223 match mention {
224 Mention::Channel(id) => {
225 #[allow(deprecated)] if let Some(channel) = id.to_channel_cached(cache) {
227 format!("#{}", channel.name).into()
228 } else {
229 "#deleted-channel".into()
230 }
231 },
232 Mention::Role(id) => options
233 .guild_reference
234 .and_then(|id| cache.guild(id))
235 .and_then(|g| g.roles.get(&id).map(|role| format!("@{}", role.name).into()))
236 .unwrap_or(Cow::Borrowed("@deleted-role")),
237 Mention::User(id) => {
238 if let Some(guild_id) = options.guild_reference {
239 if let Some(guild) = cache.guild(guild_id) {
240 if let Some(member) = guild.members.get(&id) {
241 return if options.show_discriminator {
242 format!("@{}", member.distinct())
243 } else {
244 format!("@{}", member.display_name())
245 }
246 .into();
247 }
248 }
249 }
250
251 let get_username = |user: &User| {
252 if options.show_discriminator {
253 format!("@{}", user.tag())
254 } else {
255 format!("@{}", user.name)
256 }
257 .into()
258 };
259
260 cache
261 .user(id)
262 .map(|u| get_username(&u))
263 .or_else(|| users.iter().find(|u| u.id == id).map(get_username))
264 .unwrap_or(Cow::Borrowed("@invalid-user"))
265 },
266 }
267}
268
269#[allow(clippy::non_ascii_literal)]
270#[cfg(test)]
271mod tests {
272 use std::sync::Arc;
273
274 use super::*;
275 use crate::model::channel::*;
276 use crate::model::guild::*;
277 use crate::model::id::{ChannelId, RoleId, UserId};
278
279 #[test]
280 fn test_content_safe() {
281 let user = User {
282 id: UserId::new(100000000000000000),
283 name: "Crab".to_string(),
284 ..Default::default()
285 };
286
287 let outside_cache_user = User {
288 id: UserId::new(100000000000000001),
289 name: "Boat".to_string(),
290 ..Default::default()
291 };
292
293 let mut guild = Guild {
294 id: GuildId::new(381880193251409931),
295 ..Default::default()
296 };
297
298 let member = Member {
299 nick: Some("Ferris".to_string()),
300 ..Default::default()
301 };
302
303 let role = Role {
304 id: RoleId::new(333333333333333333),
305 name: "ferris-club-member".to_string(),
306 ..Default::default()
307 };
308
309 let channel = GuildChannel {
310 id: ChannelId::new(111880193700067777),
311 name: "general".to_string(),
312 ..Default::default()
313 };
314
315 let cache = Arc::new(Cache::default());
316
317 guild.channels.insert(channel.id, channel.clone());
318 guild.members.insert(user.id, member.clone());
319 guild.roles.insert(role.id, role);
320 cache.users.insert(user.id, user.clone());
321 cache.guilds.insert(guild.id, guild.clone());
322 cache.channels.insert(channel.id, guild.id);
323
324 let with_user_mentions = "<@!100000000000000000> <@!000000000000000000> <@123> <@!123> \
325 <@!123123123123123123123> <@123> <@123123123123123123> <@!invalid> \
326 <@invalid> <@日本語 한국어$§)[/__#\\(/&2032$§#> \
327 <@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \
328 <@123invalid> <@> <@ ";
329
330 let without_user_mentions = "@Crab <@!000000000000000000> @invalid-user @invalid-user \
331 <@!123123123123123123123> @invalid-user @invalid-user <@!invalid> \
332 <@invalid> <@日本語 한국어$§)[/__#\\(/&2032$§#> \
333 <@!i)/==(<<>z/9080)> <@!1231invalid> <@invalid123> \
334 <@123invalid> <@> <@ ";
335
336 let options = ContentSafeOptions::default();
338 assert_eq!(without_user_mentions, content_safe(&cache, with_user_mentions, &options, &[]));
339
340 let options = ContentSafeOptions::default();
341 assert_eq!(
342 format!("@{}", user.name),
343 content_safe(&cache, "<@!100000000000000000>", &options, &[])
344 );
345
346 let options = ContentSafeOptions::default();
347 assert_eq!(
348 format!("@{}", user.name),
349 content_safe(&cache, "<@100000000000000000>", &options, &[])
350 );
351
352 let options = ContentSafeOptions::default();
353 assert_eq!("@invalid-user", content_safe(&cache, "<@100000000000000001>", &options, &[]));
354
355 let options = ContentSafeOptions::default();
356 assert_eq!(
357 format!("@{}", outside_cache_user.name),
358 content_safe(&cache, "<@100000000000000001>", &options, &[outside_cache_user])
359 );
360
361 let options = options.show_discriminator(false);
362 assert_eq!(
363 format!("@{}", user.name),
364 content_safe(&cache, "<@!100000000000000000>", &options, &[])
365 );
366
367 let options = options.show_discriminator(false);
368 assert_eq!(
369 format!("@{}", user.name),
370 content_safe(&cache, "<@100000000000000000>", &options, &[])
371 );
372
373 let options = options.display_as_member_from(guild.id);
374 assert_eq!(
375 format!("@{}", member.nick.unwrap()),
376 content_safe(&cache, "<@!100000000000000000>", &options, &[])
377 );
378
379 let options = options.clean_user(false);
380 assert_eq!(with_user_mentions, content_safe(&cache, with_user_mentions, &options, &[]));
381
382 let with_channel_mentions = "<#> <#deleted-channel> #deleted-channel <#1> \
384 #unsafe-club <#111880193700067777> <#ferrisferrisferris> \
385 <#000000000000000001>";
386
387 let without_channel_mentions = "<#> <#deleted-channel> #deleted-channel \
388 #deleted-channel #unsafe-club #general <#ferrisferrisferris> \
389 #deleted-channel";
390
391 assert_eq!(
392 without_channel_mentions,
393 content_safe(&cache, with_channel_mentions, &options, &[])
394 );
395
396 let options = options.clean_channel(false);
397 assert_eq!(
398 with_channel_mentions,
399 content_safe(&cache, with_channel_mentions, &options, &[])
400 );
401
402 let with_role_mentions = "<@&> @deleted-role <@&9829> \
404 <@&333333333333333333> <@&000000000000000001> \
405 <@&111111111111111111111111111111> <@&<@&1234>";
406
407 let without_role_mentions = "<@&> @deleted-role @deleted-role \
408 @ferris-club-member @deleted-role \
409 <@&111111111111111111111111111111> <@&@deleted-role";
410
411 assert_eq!(without_role_mentions, content_safe(&cache, with_role_mentions, &options, &[]));
412
413 let options = options.clean_role(false);
414 assert_eq!(with_role_mentions, content_safe(&cache, with_role_mentions, &options, &[]));
415
416 let with_everyone_mention = "@everyone";
418
419 let without_everyone_mention = "@\u{200B}everyone";
420
421 assert_eq!(
422 without_everyone_mention,
423 content_safe(&cache, with_everyone_mention, &options, &[])
424 );
425
426 let options = options.clean_everyone(false);
427 assert_eq!(
428 with_everyone_mention,
429 content_safe(&cache, with_everyone_mention, &options, &[])
430 );
431
432 let with_here_mention = "@here";
434
435 let without_here_mention = "@\u{200B}here";
436
437 assert_eq!(without_here_mention, content_safe(&cache, with_here_mention, &options, &[]));
438
439 let options = options.clean_here(false);
440 assert_eq!(with_here_mention, content_safe(&cache, with_here_mention, &options, &[]));
441 }
442}