char/phoebe

git clone https://git.t4t.associates/char/phoebe

videogame hackerForward image attachments from Discord to Matrix0649ea0

legacy
11.4 KiB385 linesraw
1use log::info;
2use serenity::{async_trait, http::AttachmentType, model::prelude::*, prelude::*};
3use tokio::sync::mpsc;
4
5use crate::{
6    channels::ChannelReference,
7    message_ast::{self, format_discord},
8    messages::{
9        DeletedMessage, EditedMessage, MessageAuthor, MessageEvent, MessageReference,
10        SentImageMessage, SentMessage,
11    },
12};
13
14pub use serenity::client::Context;
15pub use serenity::model::id::{ChannelId, MessageId};
16
17impl From<&Message> for MessageReference {
18    fn from(message: &Message) -> Self {
19        Self::Discord(message.channel_id.0, message.id.0)
20    }
21}
22
23struct DiscordHandler {
24    ctx_tx: mpsc::UnboundedSender<Context>,
25    event_tx: mpsc::UnboundedSender<MessageEvent>,
26}
27
28async fn get_message_author(ctx: &Context, message: &Message) -> MessageAuthor {
29    async fn user_tag_color(ctx: &Context, message: &Message) -> Option<String> {
30        Some(
31            message
32                .member(ctx)
33                .await
34                .ok()?
35                .colour(ctx)
36                .await?
37                .hex()
38                .to_ascii_lowercase(),
39        )
40    }
41
42    MessageAuthor {
43        display_name: message
44            .author_nick(ctx)
45            .await
46            .unwrap_or_else(|| message.author.name.clone()),
47        avatar_url: message
48            .author
49            .static_avatar_url()
50            .unwrap_or_else(|| message.author.default_avatar_url()),
51        service_name: "discord".to_string(),
52        display_color: user_tag_color(ctx, message).await,
53    }
54}
55
56#[async_trait]
57impl EventHandler for DiscordHandler {
58    async fn ready(&self, ctx: Context, _ready: Ready) {
59        let _ = self.ctx_tx.send(ctx);
60        info!("Discord ready!");
61    }
62
63    async fn message(&self, ctx: Context, message: Message) {
64        // TODO: Replace with proper management system for linking channels together
65        if let Some(target) = message.content.strip_prefix("phoebe!link ") {
66            if message
67                .member(&ctx)
68                .await
69                .unwrap()
70                .roles(&ctx)
71                .await
72                .unwrap()
73                .iter()
74                .any(|r| r.name == "Phoebe")
75            {
76                let _ = self.event_tx.send(MessageEvent::AdminLinkChannels(vec![
77                    ChannelReference::Discord(message.channel_id.0),
78                    ChannelReference::Matrix(target.to_string()),
79                ]));
80
81                message.reply(&ctx, "Linking with matrix.").await.unwrap();
82                return;
83            }
84        }
85
86        let message_ref = MessageReference::from(&message);
87
88        let content = discord_message_format::parse(&message.content);
89        let content = message_ast::convert_discord(&content);
90
91        let replies_to = message
92            .referenced_message
93            .as_ref()
94            .map(|m| MessageReference::from(m.as_ref()));
95
96        let _ = self
97            .event_tx
98            .send(MessageEvent::SendText(Box::new(SentMessage {
99                source: message_ref,
100                content,
101                author: get_message_author(&ctx, &message).await,
102                replies_to,
103            })));
104
105        for attachment in message.attachments.iter() {
106            let _ = self
107                .event_tx
108                .send(MessageEvent::SendImage(Box::new(SentImageMessage {
109                    source: MessageReference::from(&message),
110                    author: get_message_author(&ctx, &message).await,
111                    image_url: attachment.proxy_url.clone(),
112                })));
113        }
114    }
115
116    async fn message_update(
117        &self,
118        ctx: Context,
119        _old_if_available: Option<Message>,
120        new: Option<Message>,
121        event: MessageUpdateEvent,
122    ) {
123        if let Ok(new_message) = {
124            if let Some(m) = new {
125                Ok(m)
126            } else {
127                event.channel_id.message(&ctx, event.id).await
128            }
129        } {
130            let message_ref = MessageReference::from(&new_message);
131
132            let content = discord_message_format::parse(&new_message.content);
133            let content = message_ast::convert_discord(&content);
134
135            let _ = self
136                .event_tx
137                .send(MessageEvent::EditText(Box::new(EditedMessage {
138                    replacing: message_ref,
139                    content,
140                    author: get_message_author(&ctx, &new_message).await,
141                })));
142        }
143    }
144
145    async fn message_delete(
146        &self,
147        _ctx: Context,
148        channel_id: ChannelId,
149        deleted_message_id: MessageId,
150        _guild_id: Option<GuildId>,
151    ) {
152        let message_ref = MessageReference::Discord(channel_id.0, deleted_message_id.0);
153        let _ = self
154            .event_tx
155            .send(MessageEvent::Delete(Box::new(DeletedMessage {
156                reference: message_ref,
157            })));
158    }
159}
160
161async fn get_webhook_for_channel(discord_ctx: &Context, channel: &ChannelId) -> Option<Webhook> {
162    if let Ok(webhooks) = channel.webhooks(discord_ctx).await {
163        for webhook in webhooks {
164            if matches!(webhook.name.as_deref(), Some("phoebe")) {
165                return Some(webhook);
166            }
167        }
168    }
169
170    None
171}
172
173async fn get_or_create_webhook_for_channel(
174    discord_ctx: &Context,
175    channel: &ChannelId,
176) -> Option<Webhook> {
177    if let Some(webhook) = get_webhook_for_channel(discord_ctx, channel).await {
178        return Some(webhook);
179    }
180
181    if let Ok(webhook) = channel.create_webhook(discord_ctx, "phoebe").await {
182        return Some(webhook);
183    }
184
185    None
186}
187
188async fn create_webhook_reply_embeds(
189    discord_ctx: &Context,
190    reply: Option<(u64, u64)>,
191) -> Vec<serde_json::Value> {
192    if let Some((channel_id, message_id)) = reply {
193        if let Ok(replied_message) = ChannelId(channel_id)
194            .message(discord_ctx, MessageId(message_id))
195            .await
196        {
197            let replied_author_name = format!(
198                "{} ↩️",
199                replied_message
200                    .author_nick(discord_ctx)
201                    .await
202                    .as_ref()
203                    .unwrap_or(&replied_message.author.name)
204            );
205
206            let reply_description = format!(
207                "**[Reply to:]({})**\n{}",
208                replied_message.id.link(
209                    ChannelId(channel_id),
210                    discord_ctx
211                        .cache
212                        .guild_channel(channel_id)
213                        .await
214                        .map(|gc| gc.guild_id)
215                ),
216                &replied_message.content
217            );
218
219            return vec![Embed::fake(|e| {
220                e.author(|a| {
221                    a.icon_url(
222                        &replied_message
223                            .author
224                            .static_avatar_url()
225                            .unwrap_or_else(|| replied_message.author.default_avatar_url()),
226                    )
227                    .name(replied_author_name)
228                })
229                .description(reply_description)
230            })];
231        }
232    }
233
234    vec![]
235}
236
237pub async fn forward_to_discord(
238    discord_ctx: &Context,
239    channel: ChannelId,
240    message: &SentMessage,
241    reply: Option<(u64, u64)>,
242) -> Option<MessageReference> {
243    if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel).await {
244        let reply_embeds = create_webhook_reply_embeds(discord_ctx, reply).await;
245
246        return webhook
247            .execute(discord_ctx, true, |w| {
248                w.content(format_discord(&message.content))
249                    .username(format!(
250                        "{} ({})",
251                        &message.author.display_name, &message.author.service_name
252                    ))
253                    .avatar_url(&message.author.avatar_url)
254                    .embeds(reply_embeds)
255            })
256            .await
257            .ok()
258            .flatten()
259            .as_ref()
260            .map(MessageReference::from);
261    }
262
263    channel
264        .send_message(discord_ctx, |m| {
265            let content = format!(
266                "{} ({}): {}",
267                &message.author.display_name,
268                &message.author.service_name,
269                format_discord(&message.content)
270            );
271
272            if let Some((channel_id, message_id)) = reply {
273                m.content(&content)
274                    .reference_message((ChannelId(channel_id), MessageId(message_id)))
275            } else {
276                m.content(&content)
277            }
278        })
279        .await
280        .as_ref()
281        .ok()
282        .map(MessageReference::from)
283}
284
285pub async fn edit_on_discord(
286    discord_ctx: &Context,
287    channel_id: ChannelId,
288    message_id: MessageId,
289    message: &EditedMessage,
290) -> Option<MessageReference> {
291    if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel_id).await {
292        return webhook
293            .edit_message(discord_ctx, message_id, |w| {
294                w.content(format_discord(&message.content))
295            })
296            .await
297            .as_ref()
298            .ok()
299            .map(MessageReference::from);
300    }
301
302    channel_id
303        .edit_message(&discord_ctx, &message_id, |m| {
304            m.content(format_discord(&message.content))
305        })
306        .await
307        .as_ref()
308        .ok()
309        .map(MessageReference::from)
310}
311
312pub async fn delete_on_discord(
313    discord_ctx: &Context,
314    channel_id: ChannelId,
315    message_id: MessageId,
316    _message: &DeletedMessage,
317) -> bool {
318    if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel_id).await {
319        return webhook
320            .delete_message(discord_ctx, message_id)
321            .await
322            .is_ok();
323    }
324
325    channel_id
326        .delete_message(&discord_ctx, &message_id)
327        .await
328        .is_ok()
329}
330
331pub async fn forward_image_to_discord(
332    discord_ctx: &Context,
333    channel_id: ChannelId,
334    image: &SentImageMessage,
335) -> Option<MessageReference> {
336    if let Some(webhook) = get_or_create_webhook_for_channel(discord_ctx, &channel_id).await {
337        return webhook
338            .execute(discord_ctx, true, |w| {
339                w.add_file(AttachmentType::Image(&image.image_url))
340                    .username(format!(
341                        "{} ({})",
342                        &image.author.display_name, &image.author.service_name
343                    ))
344                    .avatar_url(&image.author.avatar_url)
345            })
346            .await
347            .ok()
348            .flatten()
349            .as_ref()
350            .map(MessageReference::from);
351    }
352
353    channel_id
354        .send_message(discord_ctx, |m| {
355            m.add_file(AttachmentType::Image(&image.image_url))
356                .content(format!(
357                    "{} ({}):",
358                    &image.author.display_name, &image.author.service_name
359                ))
360        })
361        .await
362        .as_ref()
363        .ok()
364        .map(MessageReference::from)
365}
366
367pub async fn create_discord_client(
368    ctx_tx: mpsc::UnboundedSender<Context>,
369    message_tx: mpsc::UnboundedSender<MessageEvent>,
370    token: &str,
371) -> Client {
372    let handler = DiscordHandler {
373        ctx_tx,
374        event_tx: message_tx,
375    };
376
377    info!("Discord logging in…");
378    let client = Client::builder(token)
379        .event_handler(handler)
380        .await
381        .expect("Failed to create discord client");
382    info!("Discord starting…");
383
384    client
385}