summaryrefslogtreecommitdiff
path: root/src/handler/pinboard.rs
blob: 02dfdf942f3ce99a2208b74198da085891a556e2 (plain)
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
use crate::utils;

use log::*;
use poise::serenity_prelude::model::prelude::*;
use poise::serenity_prelude::Context;

#[derive(Clone)]
pub struct PinBoard {
	sources: Option<Vec<ChannelId>>,
	target: ChannelId,
}
impl PinBoard {
	pub fn new() -> Option<Self> {
		let Some(target) = utils::parse_snowflake_from_env("PIN_BOARD_TARGET", ChannelId) else {
			return None;
		};
		let sources = utils::parse_snowflakes_from_env("PIN_BOARD_SOURCES", ChannelId);

		Some(Self { sources, target })
	}

	pub async fn handle_pin(&self, ctx: &Context, pin: &ChannelPinsUpdateEvent) {
		if let Some(sources) = &self.sources {
			if !sources.contains(&pin.channel_id) {
				warn!("can't access source of pin!");
				return;
			}
		}

		let mut pinner = guess_pinner(ctx, pin).await;
		let pins = pin
			.channel_id
			.pins(&ctx.http)
			.await
			.expect("couldn't get a list of pins!?");

		for pin in pins {
			// We call `take` because it's supposed to be just for the latest message.
			self.redirect(ctx, &pin, pinner.take()).await;
			pin.unpin(&ctx).await.expect("couldn't unpin message");
		}
	}

	async fn redirect(&self, ctx: &Context, pin: &Message, pinner: Option<UserId>) {
		let pinner = pinner.map_or("*someone*".to_owned(), |u| format!("<@{u}>"));

		let truncation_point = utils::floor_char_boundary(&pin.content, 700);
		let truncated_content = if pin.content.len() <= truncation_point {
			pin.content.to_string()
		} else {
			format!("{}...", &pin.content[..truncation_point])
		};
		let color = pin
			.member(ctx)
			.await
			.ok()
			.and_then(|m| m.highest_role_info(&ctx.cache))
			.and_then(|(role, _)| role.to_role_cached(&ctx.cache))
			.map(|role| role.colour);

		self.target
			.send_message(&ctx.http, |m| {
				m.allowed_mentions(|am| am.empty_parse())
					.content(format!("📌'd by {pinner} in {}", pin.link()))
					.add_embed(|embed| {
						embed.author(|author| {
							author.name(&pin.author.name).icon_url(pin.author.face())
						});

						if let Some(color) = color {
							embed.color(color);
						}
						embed.description(truncated_content)
					})
			})
			.await
			.expect("couldn't redirect message");
	}
}

/// (Desperate, best-effort) attempt to get the user that pinned the last message
///
/// Now, since Discord is SUPER annoying, it doesn't actually tell you which bloody user
/// that triggered the pins update event. So, you have to dig into the audit log.
/// Unfortunately, while you do get a timestamp, the REST API does not return the time at
/// which each action is logged, which, to me, means that it is not a freaking log *at all*.
///
/// I love Discord.
///
/// So, the plan is that only the first pinned message gets clear pinner information,
/// since we can just get the latest pin, which should happen on the exact second.
/// We can't reliably say the same for any existing pins, so we can only /shrug and say
/// *somebody* did it. Ugh.
async fn guess_pinner(ctx: &Context, pin: &ChannelPinsUpdateEvent) -> Option<UserId> {
	if let Some(g) = pin.guild_id {
		g.audit_logs(
			&ctx.http,
			// This `num` call shouldn't be necessary.
			// See https://github.com/serenity-rs/serenity/issues/2488
			Some(Action::Message(MessageAction::Pin).num()),
			None,    // user id
			None,    // before
			Some(1), // limit
		)
		.await
		.ok()
		.and_then(|mut logs| logs.entries.pop())
		.map(|first| first.user_id)
	} else {
		// TODO: mayyyyybe we can guess who pinned something in a DM...?
		warn!("couldn't figure out who pinned in {}!", pin.channel_id);
		None
	}
}