aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarald Eilertsen <haraldei@anduin.net>2024-01-08 19:12:02 +0100
committerHarald Eilertsen <haraldei@anduin.net>2024-01-08 19:27:38 +0100
commit5349a0c4320949d803df0dd8596ba8aa5497c81c (patch)
treeb7eccc741e9703b2f1150e18f3010ddf11192d87
parentd9375da4ebf466b6cbcac3b919b7226f0617a9c7 (diff)
downloadrust-zotapi-5349a0c4320949d803df0dd8596ba8aa5497c81c.tar.gz
rust-zotapi-5349a0c4320949d803df0dd8596ba8aa5497c81c.tar.bz2
rust-zotapi-5349a0c4320949d803df0dd8596ba8aa5497c81c.zip
Add high level stream API.
-rw-r--r--src/bin/zot/main.rs19
-rw-r--r--src/lib.rs4
-rw-r--r--src/stream.rs231
3 files changed, 253 insertions, 1 deletions
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 <http://www.gnu.org/licenses/>.
*/
+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 <https://www.gnu.org/licenses/>.
+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 <haraldei@anduin.net>
+ *
+ * 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<StreamItem>,
+}
+
+#[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<D>(deserializer: D) -> Result<Verb, D::Error>
+ 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<E>(self, s: &str) -> Result<Self::Value, E>
+ 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<Self, Box<dyn Error + 'static>> {
+ let items: Vec<StreamItem> = 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);
+ }
+
+ }
+}