char/phoebe

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

videogame hackerFix borrowing issue for matrix82bff1b

legacy
15.1 KiB487 linesraw
1use std::sync::Arc;
2
3use matrix_sdk::{
4    config::ClientConfig,
5    media::MediaEventContent,
6    room::{Joined, Room},
7    ruma::{
8        api,
9        events::{
10            self,
11            room::{
12                message::{
13                    FormattedBody, MessageEventContent, MessageFormat, MessageType, Relation,
14                    Replacement,
15                },
16                redaction::{RedactionEventContent, SyncRedactionEvent},
17            },
18            AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent, AnySyncRoomEvent,
19            SyncMessageEvent,
20        },
21        UserId,
22    },
23};
24pub use matrix_sdk::{
25    config::SyncSettings,
26    ruma::{EventId, RoomId},
27    Client,
28};
29
30use log::info;
31use tokio::sync::mpsc;
32use url::Url;
33
34use crate::{
35    message_ast::{
36        convert_matrix, convert_plain, format_discord, format_matrix, MessageComponent,
37        MessageContent,
38    },
39    messages::{
40        DeletedMessage, EditedMessage, MessageAuthor, MessageEvent, MessageReference,
41        SentImageMessage, SentMessage,
42    },
43};
44
45impl From<(&RoomId, &EventId)> for MessageReference {
46    fn from((room_id, event_id): (&RoomId, &EventId)) -> Self {
47        let room_string = room_id.as_str().to_string();
48        let event_string = event_id.as_str().to_string();
49
50        Self::Matrix(room_string, event_string)
51    }
52}
53
54fn _find_content(event: &AnySyncRoomEvent) -> Option<AnyMessageEventContent> {
55    match event {
56        AnySyncRoomEvent::Message(message) => Some(message.content()),
57        AnySyncRoomEvent::RedactedMessage(message) => {
58            if let Some(ref redaction_event) = message.unsigned().redacted_because {
59                Some(AnyMessageEventContent::RoomRedaction(
60                    redaction_event.content.clone(),
61                ))
62            } else {
63                Some(AnyMessageEventContent::RoomRedaction(
64                    RedactionEventContent::new(),
65                ))
66            }
67        }
68        AnySyncRoomEvent::RedactedState(state) => {
69            if let Some(ref redaction_event) = state.unsigned().redacted_because {
70                Some(AnyMessageEventContent::RoomRedaction(
71                    redaction_event.content.clone(),
72                ))
73            } else {
74                Some(AnyMessageEventContent::RoomRedaction(
75                    RedactionEventContent::new(),
76                ))
77            }
78        }
79
80        _ => None,
81    }
82}
83
84struct MatrixHandler {
85    message_tx: mpsc::UnboundedSender<MessageEvent>,
86}
87
88impl MatrixHandler {
89    async fn get_message_author(&self, room: &Joined, user_id: &UserId) -> Option<MessageAuthor> {
90        if let Ok(Some(sender)) = room.get_member(user_id).await {
91            Some(MessageAuthor {
92                display_name: sender
93                    .display_name()
94                    .unwrap_or_else(|| sender.name())
95                    .to_string(),
96                avatar_url: sender
97                    .avatar_url()
98                    .map(|u| {
99                        format!(
100                            "https://matrix.org/_matrix/media/r0/thumbnail/{}?width=500&height=500",
101                            u.as_str().to_string().replace("mxc://", "")
102                        )
103                    })
104                    .unwrap_or_else(|| {
105                        "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
106                            .to_string()
107                    }),
108                service_name: "matrix".to_string(),
109                display_color: None,
110            })
111        } else {
112            None
113        }
114    }
115
116    fn get_content(&self, body: &str, formatted_body: &Option<FormattedBody>) -> MessageContent {
117        if let Some(html) = formatted_body
118            .as_ref()
119            .filter(|f| f.format == MessageFormat::Html)
120            .map(|f| &f.body)
121        {
122            convert_matrix(html)
123        } else {
124            convert_plain(body)
125        }
126    }
127}
128
129async fn get_room_message_event(
130    room: &Joined,
131    event_id: &EventId,
132) -> Option<events::MessageEvent<MessageEventContent>> {
133    let event = room
134        .event(api::client::r0::room::get_room_event::Request::new(
135            room.room_id(),
136            event_id,
137        ))
138        .await
139        .ok()?
140        .event
141        .deserialize()
142        .ok()?;
143
144    if let AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message_event)) = event {
145        Some(message_event)
146    } else {
147        None
148    }
149}
150
151async fn on_room_message_event(
152    client: Client,
153    ctx: Arc<MatrixHandler>,
154    event: SyncMessageEvent<MessageEventContent>,
155    room: Room,
156) {
157    if let Room::Joined(room) = room {
158        if let Some(Relation::Replacement(replacement)) = &event.content.relates_to {
159            on_message_edited(ctx, &event, room, replacement).await;
160        } else {
161            on_message_sent(client, ctx, &event, room).await;
162        }
163    }
164}
165
166async fn on_message_sent(
167    client: Client,
168    ctx: Arc<MatrixHandler>,
169    event: &SyncMessageEvent<MessageEventContent>,
170    room: Joined,
171) {
172    let message_ref = MessageReference::from((room.room_id(), &event.event_id));
173
174    if let Some(author) = ctx.get_message_author(&room, &event.sender).await {
175        let message_event = match &event.content.msgtype {
176            MessageType::Text(text) => {
177                let content = ctx.get_content(&text.body, &text.formatted);
178
179                let replies_to =
180                    if let Some(Relation::Reply { in_reply_to }) = &event.content.relates_to {
181                        Some(MessageReference::from((
182                            room.room_id(),
183                            &in_reply_to.event_id,
184                        )))
185                    } else {
186                        None
187                    };
188
189                Some(MessageEvent::SendText(Box::new(SentMessage {
190                    source: message_ref,
191                    content,
192                    author,
193                    replies_to,
194                })))
195            }
196
197            MessageType::Emote(emote) => {
198                let mut content = ctx.get_content(&emote.body, &emote.formatted);
199                content.insert(0, MessageComponent::Plain("* ".to_string()));
200
201                Some(MessageEvent::SendText(Box::new(SentMessage {
202                    source: message_ref,
203                    content,
204                    author,
205                    replies_to: None,
206                })))
207            }
208
209            MessageType::Image(content) => {
210                if let Some(file) = content.file() {
211                    match file {
212                        matrix_sdk::media::MediaType::Uri(uri) => {
213                            if let Some((server_name, hash)) = uri.parts() {
214                                let server_name = server_name.as_str();
215
216                                let image_url = format!(
217                                    "https://{}/_matrix/media/r0/download/{}/{}",
218                                    server_name, server_name, hash
219                                );
220
221                                Some(MessageEvent::SendImage(Box::new(SentImageMessage {
222                                    source: message_ref,
223                                    author,
224                                    image_url,
225                                })))
226                            } else {
227                                None
228                            }
229                        }
230
231                        matrix_sdk::media::MediaType::Encrypted(encrypted_file) => {
232                            if let Some(file_data) =
233                                client.get_file(content.clone(), true).await.ok().flatten()
234                            {
235                                todo!("Reupload encrypted file to publicly accessible URL")
236                            } else {
237                                None
238                            }
239                        }
240                    }
241                } else {
242                    None
243                }
244            }
245
246            // TODO: Handle reactions, more uploads (audio, video, file), and any other types of event
247            _ => None,
248        };
249
250        if let Some(e) = message_event {
251            let _ = ctx.message_tx.send(e);
252        }
253    }
254
255    let _ = room.read_receipt(&event.event_id).await;
256}
257
258async fn on_message_edited(
259    ctx: Arc<MatrixHandler>,
260    event: &SyncMessageEvent<MessageEventContent>,
261    room: Joined,
262    replacement: &Replacement,
263) {
264    let message_ref = MessageReference::from((room.room_id(), &replacement.event_id));
265
266    if let MessageType::Text(text) = &replacement.new_content.msgtype {
267        let content = ctx.get_content(&text.body, &text.formatted);
268
269        if let Some(author) = ctx.get_message_author(&room, &event.sender).await {
270            let _ = ctx
271                .message_tx
272                .send(MessageEvent::EditText(Box::new(EditedMessage {
273                    replacing: message_ref,
274                    content,
275                    author,
276                })));
277        }
278    }
279}
280
281async fn on_redact_event(ctx: Arc<MatrixHandler>, event: SyncRedactionEvent, room: Room) {
282    let message_ref = MessageReference::from((room.room_id(), &event.redacts));
283
284    let _ = ctx
285        .message_tx
286        .send(MessageEvent::Delete(Box::new(DeletedMessage {
287            reference: message_ref,
288        })));
289}
290
291fn generate_html_content(content: &[MessageComponent], author: &MessageAuthor) -> (String, String) {
292    let plaintext_message = format!(
293        "{} ({}): {}",
294        &author.display_name,
295        &author.service_name,
296        format_discord(content)
297    );
298
299    let html_message = format!(
300        r##"<strong><font data-mx-color="#{}">{} <small>({})</small></font>:</strong> {}"##,
301        author.display_color.as_deref().unwrap_or("ffffff"),
302        &author.display_name,
303        &author.service_name,
304        format_matrix(content),
305    );
306
307    (plaintext_message, html_message)
308}
309
310fn generate_event_content_struct(
311    replied_message_event: Option<&events::MessageEvent<MessageEventContent>>,
312    plaintext_content: String,
313    html_content: String,
314) -> MessageEventContent {
315    if let Some(replied_message_event) = replied_message_event {
316        MessageEventContent::text_reply_html(plaintext_content, html_content, replied_message_event)
317    } else {
318        MessageEventContent::text_html(plaintext_content, html_content)
319    }
320}
321
322pub async fn forward_to_matrix(
323    client: &Client,
324    room_id: RoomId,
325    message: &SentMessage,
326    replying_to: Option<EventId>,
327) -> Option<MessageReference> {
328    if let Some(room) = client.get_joined_room(&room_id) {
329        let replied_message_event = if let Some(event_id) = replying_to {
330            get_room_message_event(&room, &event_id).await
331        } else {
332            None
333        };
334
335        let (plaintext_content, html_content) =
336            generate_html_content(&message.content, &message.author);
337
338        let content = generate_event_content_struct(
339            replied_message_event.as_ref(),
340            plaintext_content,
341            html_content,
342        );
343
344        let event = room
345            .send(AnyMessageEventContent::RoomMessage(content), None)
346            .await
347            .ok()?;
348
349        return Some(MessageReference::from((&room_id, &event.event_id)));
350    }
351
352    None
353}
354
355pub async fn forward_image_to_matrix(
356    client: &Client,
357    room_id: RoomId,
358    message: &SentImageMessage,
359) -> Option<MessageReference> {
360    if let Some(room) = client.get_joined_room(&room_id) {
361        let image_url = message.image_url.clone();
362        let mut response = tokio::task::spawn_blocking(|| reqwest::blocking::get(image_url).ok())
363            .await
364            .ok()
365            .flatten()?;
366
367        let event = room
368            .send_attachment(
369                &message.image_url,
370                &mime_guess::from_path(&message.image_url).first_or_octet_stream(),
371                &mut response,
372                None,
373            )
374            .await
375            .ok()?;
376
377        return Some(MessageReference::from((&room_id, &event.event_id)));
378    }
379
380    None
381}
382
383pub async fn edit_on_matrix(
384    client: &Client,
385    room_id: RoomId,
386    event_id: EventId,
387    message: &EditedMessage,
388) -> Option<MessageReference> {
389    if let Some(room) = client.get_joined_room(&room_id) {
390        if let Some(original_message_event) = get_room_message_event(&room, &event_id).await {
391            if let Some(Relation::Replacement(_)) = &original_message_event.content.relates_to {
392                return None;
393            }
394
395            let replied_message_event = if let Some(Relation::Reply { in_reply_to }) =
396                &original_message_event.content.relates_to
397            {
398                get_room_message_event(&room, &in_reply_to.event_id).await
399            } else {
400                None
401            };
402
403            let (plaintext_content, html_content) =
404                generate_html_content(&message.content, &message.author);
405
406            let mut edit_content = generate_event_content_struct(
407                replied_message_event.as_ref(),
408                format!("* {}", &plaintext_content),
409                format!("* {}", &html_content),
410            );
411
412            let basic_content = generate_event_content_struct(
413                replied_message_event.as_ref(),
414                plaintext_content,
415                html_content,
416            );
417
418            edit_content.relates_to = Some(Relation::Replacement(Replacement::new(
419                event_id,
420                Box::new(basic_content),
421            )));
422
423            let new_event = room
424                .send(AnyMessageEventContent::RoomMessage(edit_content), None)
425                .await
426                .ok()?;
427
428            return Some(MessageReference::from((&room_id, &new_event.event_id)));
429        }
430    }
431
432    None
433}
434
435pub async fn delete_on_matrix(
436    client: &Client,
437    room_id: RoomId,
438    event_id: EventId,
439    _message: &DeletedMessage,
440) -> bool {
441    if let Some(room) = client.get_joined_room(&room_id) {
442        room.redact(&event_id, None, None).await.is_ok()
443    } else {
444        false
445    }
446}
447
448pub async fn create_matrix_client(
449    homeserver_url: String,
450    username: String,
451    password: String,
452    message_tx: mpsc::UnboundedSender<MessageEvent>,
453) -> Client {
454    let client_config = ClientConfig::new().store_path("./data/matrix_state");
455
456    let homeserver_url =
457        Url::parse(&homeserver_url).expect("Failed to parse the matrix homeserver URL");
458    let client = Client::new_with_config(homeserver_url, client_config).unwrap();
459
460    info!("Matrix logging in…");
461    client
462        .login(&username, &password, None, Some("phoebe"))
463        .await
464        .expect("Failed to log in");
465
466    info!("Matrix starting…");
467    client.sync_once(SyncSettings::default()).await.unwrap();
468
469    let event_handler = Arc::new(MatrixHandler { message_tx });
470
471    {
472        let on_msg_ctx = event_handler.clone();
473        let client_2 = client.clone();
474        client
475            .register_event_handler(move |ev, room| {
476                on_room_message_event(client_2.clone(), on_msg_ctx.clone(), ev, room)
477            })
478            .await;
479    }
480
481    let on_redact_ctx = event_handler.clone();
482    client
483        .register_event_handler(move |ev, room| on_redact_event(on_redact_ctx.clone(), ev, room))
484        .await;
485
486    client
487}