209 lines
7.2 KiB
Rust
209 lines
7.2 KiB
Rust
//! 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<Post, MmError> {
|
|
// 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: <id quoted with {:?}>").
|
|
if !valid_id(&self.id) || !valid_id(&self.user_id) || !valid_id(&self.channel_id) {
|
|
return Err(MmError::Json(format!(
|
|
"a post with an invalid id: {:?}",
|
|
self.id
|
|
)));
|
|
}
|
|
if !self.root_id.is_empty() && !valid_id(&self.root_id) {
|
|
return Err(MmError::Json(format!(
|
|
"a post with an invalid id: {:?}",
|
|
self.root_id
|
|
)));
|
|
}
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
/// 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<String, serde_json::Value>,
|
|
}
|
|
|
|
pub(crate) fn json<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, MmError> {
|
|
// `serde_json::from_slice`; its error becomes Json(e.to_string()).
|
|
serde_json::from_slice(bytes).map_err(|e| MmError::Json(e.to_string()))
|
|
}
|
|
|
|
/// One WebSocket text message as an event. `posted` carries the post as a JSON **string**.
|
|
pub fn parse_event(text: &str) -> Result<Event, MmError> {
|
|
// 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("")).
|
|
let raw: RawEvent = json(text.as_bytes())?;
|
|
match raw.event.as_str() {
|
|
"hello" => Ok(Event::Hello),
|
|
"posted" => {
|
|
let post = raw
|
|
.data
|
|
.get("post")
|
|
.and_then(|p| p.as_str())
|
|
.ok_or_else(|| {
|
|
MmError::Json("a posted event carries no post string".to_string())
|
|
})?;
|
|
let post: Post = json(post.as_bytes())?;
|
|
let channel_type = raw
|
|
.data
|
|
.get("channel_type")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
Ok(Event::Posted {
|
|
post: post.check()?,
|
|
channel_type,
|
|
})
|
|
}
|
|
other => Ok(Event::Other(other.to_string())),
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
serde_json::json!({
|
|
"action": "user_typing",
|
|
"seq": seq,
|
|
"data": {"channel_id": channel, "parent_id": parent}
|
|
})
|
|
.to_string()
|
|
}
|
|
|
|
/// 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<Post>,
|
|
/// Mattermost's limit was reached: some posts may be missing.
|
|
pub full: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PostList {
|
|
#[serde(default)]
|
|
order: Vec<String>,
|
|
#[serde(default)]
|
|
posts: std::collections::HashMap<String, Post>,
|
|
}
|
|
|
|
/// The body of `GET /channels/{id}/posts?since=<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<Since, MmError> {
|
|
// 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.
|
|
let list: PostList = json(body)?;
|
|
let full = list.order.len() >= SINCE_LIMIT;
|
|
let mut seen = std::collections::HashSet::new();
|
|
let mut posts = Vec::new();
|
|
for id in &list.order {
|
|
let post = match list.posts.get(id) {
|
|
Some(p) => p.clone(),
|
|
None => continue,
|
|
};
|
|
let post = post.check()?;
|
|
if post.create_at <= since || post.delete_at != 0 {
|
|
continue;
|
|
}
|
|
if seen.insert(post.id.clone()) {
|
|
posts.push(post);
|
|
}
|
|
}
|
|
posts.sort_by(|a, b| a.create_at.cmp(&b.create_at).then_with(|| a.id.cmp(&b.id)));
|
|
Ok(Since { posts, full })
|
|
}
|