aboutsummaryrefslogblamecommitdiffstats
path: root/src/stream.rs
blob: 475c886f3425872dd66be9e50911bd5a5effc1b3 (plain) (tree)






































































































































































































































                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
/**
 * 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);
        }

    }
}