use crate::controllers::HttpResult; use crate::controllers::matrix::matrix_media_controller; use crate::controllers::matrix::matrix_media_controller::MediaQuery; use crate::controllers::matrix::matrix_room_controller::RoomIdInPath; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; use matrix_sdk::Room; use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::media::MediaEventContent; use matrix_sdk::room::MessagesOptions; use matrix_sdk::room::edit::EditedContent; use matrix_sdk::ruma::events::reaction::ReactionEventContent; use matrix_sdk::ruma::events::relation::Annotation; use matrix_sdk::ruma::events::room::message::{ MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, }; use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent}; use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; #[derive(Serialize)] pub struct APIEvent { id: OwnedEventId, time: MilliSecondsSinceUnixEpoch, sender: OwnedUserId, data: Box, } impl APIEvent { pub async fn from_evt(msg: TimelineEvent, room_id: &RoomId) -> anyhow::Result { let (event, raw) = match &msg.kind { TimelineEventKind::Decrypted(d) => (d.event.deserialize()?, d.event.json()), TimelineEventKind::UnableToDecrypt { event, .. } | TimelineEventKind::PlainText { event } => ( event.deserialize()?.into_full_event(room_id.to_owned()), event.json(), ), }; Ok(Self { id: event.event_id().to_owned(), time: event.origin_server_ts(), sender: event.sender().to_owned(), data: raw.to_owned(), }) } } #[derive(Serialize)] pub struct APIEventsList { pub start: String, pub end: Option, pub events: Vec, } /// Get messages for a given room pub(super) async fn get_events( room: &Room, limit: u32, from: Option<&str>, ) -> anyhow::Result { let mut msg_opts = MessagesOptions::backward(); msg_opts.from = from.map(str::to_string); msg_opts.limit = UInt::from(limit); let messages = room.messages(msg_opts).await?; Ok(APIEventsList { start: messages.start, end: messages.end, events: stream::iter(messages.chunk) .then(async |msg| APIEvent::from_evt(msg, room.room_id()).await) .collect::>() .await .into_iter() .collect::, _>>()?, }) } #[derive(Deserialize)] pub struct GetRoomEventsQuery { #[serde(default)] limit: Option, #[serde(default)] from: Option, } /// Get the events for a room pub async fn get_for_room( client: MatrixClientExtractor, path: web::Path, query: web::Query, ) -> HttpResult { let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; Ok(HttpResponse::Ok() .json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?)) } #[derive(Deserialize)] struct SendTextMessageRequest { content: String, } pub async fn send_text_message( client: MatrixClientExtractor, path: web::Path, ) -> HttpResult { let req = client.auth.decode_json_body::()?; let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; room.send(RoomMessageEventContent::text_plain(req.content)) .await?; Ok(HttpResponse::Accepted().finish()) } #[derive(serde::Deserialize)] pub struct EventIdInPath { pub(crate) event_id: OwnedEventId, } pub async fn set_text_content( client: MatrixClientExtractor, path: web::Path, event_path: web::Path, ) -> HttpResult { let req = client.auth.decode_json_body::()?; let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; let edit_event = match room .make_edit_event( &event_path.event_id, EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( req.content, )), ) .await { Ok(msg) => msg, Err(e) => { log::error!( "Failed to created edit message event {}: {e}", event_path.event_id ); return Ok(HttpResponse::InternalServerError() .json(format!("Failed to create edit message event! {e}"))); } }; Ok(match room.send(edit_event).await { Ok(_) => HttpResponse::Accepted().finish(), Err(e) => { log::error!("Failed to edit event message {}: {e}", event_path.event_id); HttpResponse::InternalServerError().json(format!("Failed to edit event! {e}")) } }) } pub async fn event_file( req: HttpRequest, client: MatrixClientExtractor, path: web::Path, event_path: web::Path, ) -> HttpResult { let query = web::Query::::from_request(&req, &mut Payload::None).await?; let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; let event = match room.load_or_fetch_event(&event_path.event_id, None).await { Ok(event) => event, Err(e) => { log::error!("Failed to load event information! {e}"); return Ok(HttpResponse::InternalServerError() .json(format!("Failed to load event information! {e}"))); } }; let event = match event.kind { TimelineEventKind::Decrypted(dec) => dec.event.deserialize()?, TimelineEventKind::UnableToDecrypt { event, .. } | TimelineEventKind::PlainText { event } => event .deserialize()? .into_full_event(room.room_id().to_owned()), }; let AnyTimelineEvent::MessageLike(message) = event else { return Ok(HttpResponse::BadRequest().json("Event is not message like!")); }; let AnyMessageLikeEvent::RoomMessage(message) = message else { return Ok(HttpResponse::BadRequest().json("Event is not a room message!")); }; let RoomMessageEvent::Original(message) = message else { return Ok(HttpResponse::BadRequest().json("Event has been redacted!")); }; let (source, thumb_source) = match message.content.msgtype { MessageType::Audio(c) => (c.source(), c.thumbnail_source()), MessageType::File(c) => (c.source(), c.thumbnail_source()), MessageType::Image(c) => (c.source(), c.thumbnail_source()), MessageType::Location(c) => (c.source(), c.thumbnail_source()), MessageType::Video(c) => (c.source(), c.thumbnail_source()), _ => (None, None), }; println!("{source:#?} {thumb_source:#?}"); let source = match (query.thumbnail, source, thumb_source) { (false, Some(s), _) => s, (true, _, Some(s)) => s, _ => return Ok(HttpResponse::NotFound().json("Requested file not available!")), }; matrix_media_controller::serve_media(req, source, false).await } #[derive(Deserialize)] struct EventReactionBody { key: String, } pub async fn react_to_event( client: MatrixClientExtractor, path: web::Path, event_path: web::Path, ) -> HttpResult { let body = client.auth.decode_json_body::()?; let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; let annotation = Annotation::new(event_path.event_id.to_owned(), body.key.to_owned()); room.send(ReactionEventContent::from(annotation)).await?; Ok(HttpResponse::Accepted().finish()) } pub async fn redact_event( client: MatrixClientExtractor, path: web::Path, event_path: web::Path, ) -> HttpResult { let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; Ok(match room.redact(&event_path.event_id, None, None).await { Ok(_) => HttpResponse::Accepted().finish(), Err(e) => { log::error!("Failed to redact event {}: {e}", event_path.event_id); HttpResponse::InternalServerError().json(format!("Failed to redact event! {e}")) } }) }