//! Mattermost's JSON, typed: the posts and users we read, the events that arrive over the //! WebSocket, and the requests we send over it. Mattermost's JSON is not ours: unknown fields are //! ignored, but every id we keep must be a valid id. pub mod rest; use std::time::Duration; use serde::Deserialize; use crate::config::valid_id; /// At most this many changed posts come back from one `posts?since` call (Mattermost v11.11.0, /// `SqlPostStore::GetPostsSince`); a full answer may have left some out. pub const SINCE_LIMIT: usize = 1000; #[derive(Debug)] pub enum MmError { /// No answer: connecting, TLS, or the HTTP exchange failed. Net(String), /// 401 or 403: the token is refused. Auth(u16), /// 429, still, after waiting as asked. RateLimited(Duration), /// Any other status that is not 2xx, with the start of the body. Status(u16, String), /// The answer is not the JSON we expect. Json(String), } impl std::fmt::Display for MmError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MmError::Net(why) => write!(f, "{why}"), MmError::Auth(status) => write!(f, "Mattermost refused the token ({status})"), MmError::RateLimited(wait) => { write!(f, "rate limited; asked to wait {} s", wait.as_secs()) } // Quoted: the body is the server's text, and must not forge a log line. MmError::Status(status, body) => write!(f, "status {status}: {body:?}"), MmError::Json(why) => write!(f, "unexpected JSON: {why}"), } } } impl std::error::Error for MmError {} /// This bot, from `GET /users/me`. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct Me { pub id: String, pub username: String, } /// The fields of a post that `gatewayd` uses. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct Post { pub id: String, pub user_id: String, pub channel_id: String, #[serde(default)] pub root_id: String, #[serde(default)] pub message: String, pub create_at: i64, #[serde(default)] pub delete_at: i64, /// Empty for a message a user wrote; anything else is a system message. #[serde(default, rename = "type")] pub kind: String, } impl Post { /// Every id is a Mattermost id (the root may be empty): they end up in session ids and paths. pub(crate) fn check(self) -> Result { // id, user_id and channel_id must be `valid_id`, and root_id empty or `valid_id`; otherwise // Json("a post with an invalid id: "). todo!() } } /// An event from the WebSocket. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event { Hello, /// A new post, and the type of its channel: `D` direct, `G` group, `O` open, `P` private. Posted { post: Post, channel_type: String, }, /// Any other event, or a reply to one of our requests, by its name (empty for a reply). Other(String), } #[derive(Deserialize)] struct RawEvent { #[serde(default)] event: String, #[serde(default)] data: serde_json::Map, } pub(crate) fn json(bytes: &[u8]) -> Result { // `serde_json::from_slice`; its error becomes Json(e.to_string()). todo!() } /// One WebSocket text message as an event. `posted` carries the post as a JSON **string**. pub fn parse_event(text: &str) -> Result { // Parse into RawEvent. "hello" -> Hello. "posted" -> data.post must be a JSON *string*; parse // that string as a Post and `check` it (a missing or non-string post is Json). channel_type is // data.channel_type when it is a string, else "". Any other event name -> Other(name) (a reply // has no event: Other("")). todo!() } /// The WebSocket request that shows this bot as typing in a thread (`parent` is the root). pub fn typing(seq: u64, channel: &str, parent: &str) -> String { // serde_json::json!({"action": "user_typing", "seq": seq, "data": {"channel_id": channel, // "parent_id": parent}}) as a string. todo!() } /// What a `posts?since` call gave back. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Since { /// Posts created after the time, not deleted, oldest first. pub posts: Vec, /// Mattermost's limit was reached: some posts may be missing. pub full: bool, } #[derive(Deserialize)] struct PostList { #[serde(default)] order: Vec, #[serde(default)] posts: std::collections::HashMap, } /// The body of `GET /channels/{id}/posts?since=`. Only ids in `order` changed after /// `since`; `posts` also holds the roots of their threads, which may be older. Edited and deleted /// posts come back too: only new posts count. pub fn since_list(body: &[u8], since: i64) -> Result { // Parse a PostList. full = order.len() >= SINCE_LIMIT. For each id in `order` (never the keys // of `posts`), skip it if it is not in `posts`; keep the post if create_at > since and // delete_at == 0, after `check`. Sort by (create_at, id), remove repeated ids. todo!() }