From a6aa6592e2699c71baf29145e097b884c4e7d8c9 Mon Sep 17 00:00:00 2001 From: mikoto Date: Sat, 27 Jan 2024 23:50:38 +0000 Subject: [PATCH] feat(core/events): legacy handler to read posts and replies from boards The naming `legacy` refers to code that will remain compatible with `commune-server` once I introduce breaking changes in the future. Note that this is a draft since I'm not sure what the idiomatic way of selecting rows from multiple tables without embedding them as JSON (which I think is an anti-pattern for queries). --- crates/core/src/db/entities/legacy.rs | 74 +++++++++++++++++++++++ crates/core/src/db/entities/mod.rs | 4 ++ crates/core/src/events/error.rs | 28 +++++++++ crates/core/src/events/mod.rs | 5 ++ crates/core/src/events/service.rs | 87 ++++++++++++++++++++++++--- 5 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 crates/core/src/db/entities/legacy.rs create mode 100644 crates/core/src/db/entities/mod.rs create mode 100644 crates/core/src/events/error.rs create mode 100644 crates/core/src/events/mod.rs diff --git a/crates/core/src/db/entities/legacy.rs b/crates/core/src/db/entities/legacy.rs new file mode 100644 index 0000000..2accfd2 --- /dev/null +++ b/crates/core/src/db/entities/legacy.rs @@ -0,0 +1,74 @@ +use matrix::events::space::BoardPostEvent; +use serde::{Deserialize, Serialize}; +use tokio_postgres::{types::Json, Error, Row}; + +#[derive(Deserialize)] +pub struct LegacyEntity { + pub event_id: String, + pub json: BoardPostEvent, + pub room_alias: String, + pub display_name: Option, + pub avatar_url: Option, + pub replies: u32, + pub slug: String, + pub reactions: Vec<()>, + pub edited_on: Option, + + // only for replies + pub in_reply_to: Option, + pub downvotes: Option, + pub upvotes: Option, + #[serde(skip_deserializing, flatten)] + pub metadata: Option, +} +#[derive(Deserialize)] +pub struct ReactionEntity { + key: String, + url: String, + senders: Vec, +} + +#[derive(Default, Serialize, Deserialize)] +#[serde(from = "LegacyEntity")] +pub struct Metadata { + pub edited: bool, + pub downvoted: bool, + pub upvoted: bool, +} + +impl From for Metadata { + fn from(inner: LegacyEntity) -> Self { + let edited = inner.edited_on.is_some(); + let downvoted = inner.downvotes.unwrap_or(0) > 0; + let upvoted = inner.upvotes.unwrap_or(0) > 0; + + Self { + edited, + downvoted, + upvoted, + } + } +} + +impl TryFrom for LegacyEntity { + type Error = Error; + + fn try_from(row: Row) -> Result { + Ok(Self { + event_id: row.try_get(0)?, + json: row.try_get(1).map(|j: Json| j.0)?, + room_alias: row.try_get(5)?, + display_name: row.try_get(3)?, + avatar_url: row.try_get(4)?, + replies: 0, + slug: row.try_get(6)?, + reactions: vec![], + edited_on: row.try_get(7).map(|n: Option| n.map(|m| m as u32))?, + + in_reply_to: None, + downvotes: None, + upvotes: None, + metadata: None, + }) + } +} diff --git a/crates/core/src/db/entities/mod.rs b/crates/core/src/db/entities/mod.rs new file mode 100644 index 0000000..617c3c1 --- /dev/null +++ b/crates/core/src/db/entities/mod.rs @@ -0,0 +1,4 @@ +pub mod legacy; +pub mod post; +pub mod reply; + diff --git a/crates/core/src/events/error.rs b/crates/core/src/events/error.rs new file mode 100644 index 0000000..9e8c02d --- /dev/null +++ b/crates/core/src/events/error.rs @@ -0,0 +1,28 @@ +use http::StatusCode; +use thiserror::Error; + +use crate::error::HttpStatusCode; + +#[derive(Debug, Error)] +pub enum EventsErrorCode { + #[error("Not found")] + NotFound(u64), + #[error("Malformed body")] + MalformedBody, +} + +impl HttpStatusCode for EventsErrorCode { + fn status_code(&self) -> StatusCode { + match self { + EventsErrorCode::NotFound(_) => StatusCode::NOT_FOUND, + EventsErrorCode::MalformedBody => StatusCode::BAD_REQUEST, + } + } + + fn error_code(&self) -> &'static str { + match self { + EventsErrorCode::NotFound(_) => "NOT_FOUND", + EventsErrorCode::MalformedBody => "BAD_REQUEST", + } + } +} diff --git a/crates/core/src/events/mod.rs b/crates/core/src/events/mod.rs new file mode 100644 index 0000000..7831f98 --- /dev/null +++ b/crates/core/src/events/mod.rs @@ -0,0 +1,5 @@ +pub mod service; +pub mod error; + +/// This is a re-export of `matrix::events`. +pub use matrix::events as ruma; diff --git a/crates/core/src/events/service.rs b/crates/core/src/events/service.rs index 2fd28d2..04cf4c9 100644 --- a/crates/core/src/events/service.rs +++ b/crates/core/src/events/service.rs @@ -1,17 +1,17 @@ -use std::{str::FromStr}; +use std::str::FromStr; use matrix::events::{ - exports::ruma_common::{OwnedEventId, TransactionId}, - relation::{InReplyTo}, + exports::ruma_common::{OwnedEventId, TransactionId, serde::Raw}, + relation::InReplyTo, room::message::Relation, - space::{BoardPostEventContent, BoardReplyEventContent}, + space::{BoardPostEventContent, BoardReplyEventContent}, AnyStateEventContent, }; use reqwest::{Client as HttpClient, Response as HttpResponse}; use serde::{Deserialize, Serialize}; use tokio_postgres::Client as PostgresClient; -use crate::{Error, Result}; +use crate::{Error, Result, db::entities::legacy::LegacyEntity}; #[derive(Serialize, Deserialize, Debug)] pub struct SendResponse { @@ -35,7 +35,7 @@ impl EventsService { } } - pub async fn new_post>( + pub async fn create_post>( &self, content: BoardPostEventContent, board_id: S, @@ -60,7 +60,7 @@ impl EventsService { Ok(data) } - pub async fn new_reply>( + pub async fn create_reply>( &self, mut content: BoardReplyEventContent, in_reply_to: S, @@ -95,6 +95,37 @@ impl EventsService { Ok(data) } + pub async fn create_state>( + &self, + content: Raw, + board_id: S, + event_type: S, + state_key: Option, + token: S, + ) -> Result { + let mut endpoint = format!( + "/_matrix/client/v3/rooms/{board_id}/state/{event_type}", + board_id = board_id.as_ref(), + event_type = event_type.as_ref(), + ); + + if let Some(_state_key) = state_key { + endpoint.push_str("/{state_key}"); + } + + let resp = self.put(endpoint, token, content).await?; + + let body = resp.bytes().await.unwrap(); + tracing::debug!(?body); + + let data: SendResponse = serde_json::from_slice(&body).map_err(|err| { + tracing::error!(?err, "Failed to deserialize response"); + Error::Unknown + })?; + + Ok(data) + } + async fn put( &self, endpoint: impl AsRef, @@ -109,4 +140,46 @@ impl EventsService { Error::Unknown }) } + + pub async fn all_events(&self) -> Result> { + let statement = " + SELECT + e.event_id, + js.json, + ms.display_name, + ms.avatar_url, + ra.room_alias, + RIGHT(e.event_id, 11) AS slug, + js.json :: json ->> 'origin_server_ts' AS edited_on, + 0 AS replies, + FROM + events e + LEFT JOIN event_json js USING(event_id) + LEFT JOIN room_aliases ra USING(room_id) + LEFT JOIN event_relations er ON er.relates_to_id = e.event_id + LEFT JOIN redactions ON redacts = e.event_id + LEFT JOIN membership_state ms ON ms.user_id = e.sender + AND ms.room_id = e.room_id + WHERE + e.origin_server_ts < $1 + AND e.type = 'space.board.post' + AND redacts IS NULL + AND aliases.room_alias IS NOT NULL + "; + + let event_rows = self.postgres.query(statement, &[]).await.map_err(|err| { + tracing::error!(?err, "Failed to query events"); + Error::Unknown + })?; + + + let result = event_rows.into_iter().map(LegacyEntity::try_from); + let ok = result.flat_map(|r| r.ok()).collect(); + + Ok(ok) + } + + pub async fn all_replies(&self) -> Result> { + unimplemented!() + } }