From 5349a0c4320949d803df0dd8596ba8aa5497c81c Mon Sep 17 00:00:00 2001 From: Harald Eilertsen Date: Mon, 8 Jan 2024 19:12:02 +0100 Subject: Add high level stream API. --- src/bin/zot/main.rs | 19 ++++- src/lib.rs | 4 + src/stream.rs | 231 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/stream.rs diff --git a/src/bin/zot/main.rs b/src/bin/zot/main.rs index 63b4557..209e097 100644 --- a/src/bin/zot/main.rs +++ b/src/bin/zot/main.rs @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +use zotapi::Stream; + use clap::{clap_app, crate_authors, crate_version}; use dotenv::dotenv; use std::env; @@ -115,7 +117,22 @@ async fn main() -> Result<(), Box<(dyn std::error::Error + 'static)>> { } } ("stream", Some(_)) => { - println!("{}", z.channel_stream().await?); + let s = Stream::from_json(&z.channel_stream().await?)?; + for item in s.items { + if item.is_post() { + if item.title.len() > 0 { + println!("# {}", item.title); + } + + if item.summary.len() > 0 { + println!("Summary: {}\n", item.summary); + } else { + println!("{}\n", item.body); + } + println!(); + } + } + //println!("{}", z.channel_stream().await?); } ("export", Some(_)) => { println!("{}", z.channel_export().await?); diff --git a/src/lib.rs b/src/lib.rs index df661f7..d1df0ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +pub mod stream; + // mod abconfig; // mod abook; // mod channel_stream; @@ -26,6 +28,8 @@ mod verify; mod xchan; mod zotapi; +pub use stream::Stream; + // pub use abconfig::ABConfig; // pub use abook::Abook; // pub use channel_stream::channel_stream; diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..475c886 --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,231 @@ +/** + * High level stream API for ZotApi. + * + * The stream API will parse the raw json response from a stream (network + * or channel stream) into proper types, that should be easier to use from + * a client application. + * + * SPDX-FileCopyrightText: 2023 Eilertsens Kodeknekkeri + * SPDX-FileCopyrightText: 2023 Harald Eilertsen + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +use serde::Deserialize; +use std::{ + error::Error, + result::Result +}; + +#[derive(Debug, PartialEq)] +pub struct Stream { + pub items: Vec, +} + +#[derive(Debug, Deserialize, PartialEq)] +enum StreamItemType { + #[serde(rename="activity")] + Activity, +} + +#[derive(Debug, Deserialize, PartialEq)] +enum StreamItemEncoding { + #[serde(rename="zot")] + Zot, +} + +#[derive(Debug, PartialEq)] +enum Verb { + Post, + Like, + Update, +} + +impl<'de> Deserialize<'de> for Verb { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(VerbVisitor) + } +} + +struct VerbVisitor; + +impl<'de> serde::de::Visitor<'de> for VerbVisitor { + type Value = Verb; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a valid activity streams verb URI") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + match s { + "http://activitystrea.ms/schema/1.0/post" => + Ok(Verb::Post), + "http://activitystrea.ms/schema/1.0/like" => + Ok(Verb::Like), + "http://activitystrea.ms/schema/1.0/update" => + Ok(Verb::Update), + _ => + Err(E::custom(format!("unknown activity streams verb"))) + } + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct StreamItem { + #[serde(rename="type")] + item_type: StreamItemType, + encoding: StreamItemEncoding, + + verb: Verb, + + pub title: String, + pub summary: String, + pub body: String, +} + +impl StreamItem { + pub fn is_post(&self) -> bool { + self.verb == Verb::Post + } +} + +impl Stream { + pub fn from_json(json: &str) -> Result> { + let items: Vec = serde_json::from_str(&json)?; + Ok(Self { items }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn construct_stream_from_empty_string_should_fail() { + let s = Stream::from_json(""); + assert!(s.is_err()); + } + + #[test] + fn construct_stream_from_empty_json() { + let s = Stream::from_json("[]"); + assert!(s.is_ok()); + } + + #[test] + fn construct_stream_from_json_with_one_activity() { + let json = r#"[ + { + "type": "activity", + "encoding": "zot", + "uuid": "2c102ec3-676b-4d84-b2b4-9141467a254f", + "message_id": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "message_top": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "message_parent": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "created": "2023-12-12 17:00:42", + "edited": "2023-12-12 17:00:42", + "expires": "0000-00-00 00:00:00", + "commented": "2023-12-19 09:01:15", + "mimetype": "text/bbcode", + "title": "The item title", + "summary": "The summary of the post", + "body": "The body of the post", + "app": "", + "verb": "http://activitystrea.ms/schema/1.0/post", + "object_type": "http://activitystrea.ms/schema/1.0/note", + "target_type": "", + "permalink": "https://example.com/item/2c102ec3-676b-4d84-b2b4-9141467a254f", + "location": "", + "longlat": "", + "signature": "sha256.ADzsjr5uFUpcwVkUT9liHimr1n2TJmGpTtm0SGSody_6vTbfYhHK3OJJhGknImHYwMradSwVcA9dNAAXzhSoAY5hCmSbnThLun30HVIA3E0ZSDBbt1RyKNomqBCvCBmyyFKhxYoPk34UQisCH6gIQ3eZL-m5PxE9t2oMO_CnpPvLWESoezY3CAZaEIIRj3KKozwC8DxibQmsnCeA32C-2Ejzv8PdZCX1skbIdc5d8Jj_ykyUTqc2DZKMSl9osua3esEMwYZLbxlRQruDfgyjJExdvz57zpeqf1WKzqdIZ5kmguZxch6NLVMiRitooT-sIZOvo9JzgP1ogBQW34W-kG1hHzuGJQkDZKOAA1rLD8qrfwugbbg9wDpjtZY-WHv265KMMsFhctIIupNPVVkuSgs0jMKQmjJTeDE0IxBKx2dCsxiqMWlJll_LSu7b6BtTa7jcuhfMPaKqdMOCRRnGNwehj6qeOXPV7zEZsc4Mzym6N_jRTV556OmUwaCHouEFqTG1ARE5EquYTMk2wXIteC5lT2d0WpEGTnlL1WpqWLx9DCp76Z2JsxMBxjZicKvJSm1gX3ng2ENG2cvECww0IF_zPy2DmHcWOqm795-11uLBNt_60bpP9-sgjrNSj0q6MHgAsFGnpW42M4sFe-6gb-W-HjyHwY2B2yjErYC9KDo", + "route": "", + "owner": { + "name": "Benjamin Franklin", + "address": "ben@example.com", + "url": "https://example.com/channel/ben", + "network": "zot6", + "photo": { + "mimetype": "image/jpeg", + "src": "https://example.com/photo/profile/m/2" + }, + "id": "rUpgk2qbvnWLoKIXOlZlwlqI5vk8C4NgudFNjbcmnOBjFSXU34TObkZEClaPSfKnpFZpg87tANtko7WGs7QRvA", + "id_sig": "sha256.ZD8uwYmUEG_d02Y...", + "key": "-----BEGIN PUBLIC KEY-----\n....\n-----END PUBLIC KEY-----\n" + }, + "author": { + "name": "Benjamin Franklin", + "address": "ben@example.com", + "url": "https://example.com/channel/ben", + "network": "zot6", + "photo": { + "mimetype": "image/jpeg", + "src": "https://example.com/photo/profile/m/2" + }, + "id": "rUpgk2qbvnWLoKIXOlZlwlqI5vk8C4NgudFNjbcmnOBjFSXU34TObkZEClaPSfKnpFZpg87tANtko7WGs7QRvA", + "id_sig": "sha256.ZD8uwYmUEG_d02Y...", + "key": "-----BEGIN PUBLIC KEY-----\n....\n-----END PUBLIC KEY-----\n" + }, + "flags": [ + "thread_parent" + ], + "public_scope": "", + "comment_scope": "authenticated", + "tags": [ + { + "tag": "a-tag", + "url": "https://example.com/search?tag=a-tag", + "type": "hashtag" + }, + { + "tag": "fediverse", + "url": "https://example.com/search?tag=fediverse", + "type": "hashtag" + }, + { + "tag": "hubzilla", + "url": "https://example.com/search?tag=hubzilla", + "type": "hashtag" + } + ] + }]"#; + let s = Stream::from_json(&json).unwrap(); + assert_eq!(1, s.items.len()); + + let item = &s.items[0]; + + assert_eq!(StreamItemType::Activity, item.item_type); + assert_eq!(StreamItemEncoding::Zot, item.encoding); + assert_eq!(Verb::Post, item.verb); + assert_eq!("The item title", &item.title); + assert_eq!("The summary of the post", &item.summary); + assert_eq!("The body of the post", &item.body); + } + + #[test] + fn deserialize_activitustreams_verb_from_json() { + #[derive(Debug, Deserialize, PartialEq)] + struct VerbTest { + verb: Verb + } + + let verbs = vec![ + (Verb::Post, "post"), + (Verb::Like, "like"), + (Verb::Update, "update") + ]; + + for v in verbs { + let verb: VerbTest = serde_json::from_str( + &format!(r#"{{"verb": "http://activitystrea.ms/schema/1.0/{}"}}"#, v.1) + ).unwrap(); + assert_eq!(v.0, verb.verb); + } + + } +} -- cgit v1.2.3