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