From a91c0c8feb9ecbc692384bc39fc36658f63fb987 Mon Sep 17 00:00:00 2001
From: Colin Reeder <vpzomtrrfrt@gmail.com>
Date: Fri, 26 Nov 2021 21:55:33 -0700
Subject: [PATCH] Allow single item for to, cc, and @context

---
 crates/apub/src/context.rs                    |  1 +
 crates/apub/src/lib.rs                        | 20 +++++++++++++++++++
 .../protocol/activities/community/add_mod.rs  |  2 ++
 .../protocol/activities/community/announce.rs |  2 ++
 .../activities/community/block_user.rs        |  2 ++
 .../activities/community/remove_mod.rs        |  2 ++
 .../activities/community/undo_block_user.rs   |  2 ++
 .../protocol/activities/community/update.rs   |  2 ++
 .../activities/create_or_update/comment.rs    |  2 ++
 .../activities/create_or_update/post.rs       |  2 ++
 .../protocol/activities/deletion/delete.rs    |  1 +
 .../activities/deletion/undo_delete.rs        |  2 ++
 .../protocol/activities/voting/undo_vote.rs   |  2 ++
 .../src/protocol/activities/voting/vote.rs    |  2 ++
 crates/apub/src/protocol/objects/note.rs      |  2 ++
 crates/apub/src/protocol/objects/page.rs      |  2 ++
 16 files changed, 48 insertions(+)

diff --git a/crates/apub/src/context.rs b/crates/apub/src/context.rs
index 3ec444e3..9d2d3d42 100644
--- a/crates/apub/src/context.rs
+++ b/crates/apub/src/context.rs
@@ -8,6 +8,7 @@ static CONTEXT: Lazy<Vec<serde_json::Value>> = Lazy::new(|| {
 #[derive(Serialize, Deserialize, Debug)]
 pub(crate) struct WithContext<T> {
   #[serde(rename = "@context")]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   context: Vec<serde_json::Value>,
   #[serde(flatten)]
   inner: T,
diff --git a/crates/apub/src/lib.rs b/crates/apub/src/lib.rs
index 984d722e..b3c7eff7 100644
--- a/crates/apub/src/lib.rs
+++ b/crates/apub/src/lib.rs
@@ -3,6 +3,7 @@ use anyhow::{anyhow, Context};
 use lemmy_api_common::blocking;
 use lemmy_db_schema::{newtypes::DbUrl, source::activity::Activity, DbPool};
 use lemmy_utils::{location_info, settings::structs::Settings, LemmyError};
+use serde::{Deserialize, Deserializer};
 use std::net::IpAddr;
 use url::{ParseError, Url};
 
@@ -85,6 +86,25 @@ pub(crate) fn check_is_apub_id_valid(
   Ok(())
 }
 
+pub(crate) fn deserialize_one_or_many<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
+where
+  T: Deserialize<'de>,
+  D: Deserializer<'de>,
+{
+  #[derive(Deserialize)]
+  #[serde(untagged)]
+  enum OneOrMany<T> {
+    One(T),
+    Many(Vec<T>),
+  }
+
+  let result: OneOrMany<T> = Deserialize::deserialize(deserializer)?;
+  Ok(match result {
+    OneOrMany::Many(list) => list,
+    OneOrMany::One(value) => vec![value],
+  })
+}
+
 pub enum EndpointType {
   Community,
   Person,
diff --git a/crates/apub/src/protocol/activities/community/add_mod.rs b/crates/apub/src/protocol/activities/community/add_mod.rs
index 0d6edc0a..915c2466 100644
--- a/crates/apub/src/protocol/activities/community/add_mod.rs
+++ b/crates/apub/src/protocol/activities/community/add_mod.rs
@@ -8,9 +8,11 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct AddMod {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: ObjectId<ApubPerson>,
   pub(crate) target: Url,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: AddType,
diff --git a/crates/apub/src/protocol/activities/community/announce.rs b/crates/apub/src/protocol/activities/community/announce.rs
index 11890c68..0bda9ebc 100644
--- a/crates/apub/src/protocol/activities/community/announce.rs
+++ b/crates/apub/src/protocol/activities/community/announce.rs
@@ -12,8 +12,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct AnnounceActivity {
   pub(crate) actor: ObjectId<ApubCommunity>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: AnnouncableActivities,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: AnnounceType,
diff --git a/crates/apub/src/protocol/activities/community/block_user.rs b/crates/apub/src/protocol/activities/community/block_user.rs
index c904fc21..ecde0ce3 100644
--- a/crates/apub/src/protocol/activities/community/block_user.rs
+++ b/crates/apub/src/protocol/activities/community/block_user.rs
@@ -11,8 +11,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct BlockUserFromCommunity {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   pub(crate) target: ObjectId<ApubCommunity>,
   #[serde(rename = "type")]
diff --git a/crates/apub/src/protocol/activities/community/remove_mod.rs b/crates/apub/src/protocol/activities/community/remove_mod.rs
index dc456b0b..74619c81 100644
--- a/crates/apub/src/protocol/activities/community/remove_mod.rs
+++ b/crates/apub/src/protocol/activities/community/remove_mod.rs
@@ -8,8 +8,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct RemoveMod {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: RemoveType,
diff --git a/crates/apub/src/protocol/activities/community/undo_block_user.rs b/crates/apub/src/protocol/activities/community/undo_block_user.rs
index f08e0c35..02218367 100644
--- a/crates/apub/src/protocol/activities/community/undo_block_user.rs
+++ b/crates/apub/src/protocol/activities/community/undo_block_user.rs
@@ -11,8 +11,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct UndoBlockUserFromCommunity {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: BlockUserFromCommunity,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: UndoType,
diff --git a/crates/apub/src/protocol/activities/community/update.rs b/crates/apub/src/protocol/activities/community/update.rs
index 9bdcded9..bb9b5611 100644
--- a/crates/apub/src/protocol/activities/community/update.rs
+++ b/crates/apub/src/protocol/activities/community/update.rs
@@ -13,9 +13,11 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct UpdateCommunity {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   // TODO: would be nice to use a separate struct here, which only contains the fields updated here
   pub(crate) object: Box<Group>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: UpdateType,
diff --git a/crates/apub/src/protocol/activities/create_or_update/comment.rs b/crates/apub/src/protocol/activities/create_or_update/comment.rs
index deef4c86..091c0f97 100644
--- a/crates/apub/src/protocol/activities/create_or_update/comment.rs
+++ b/crates/apub/src/protocol/activities/create_or_update/comment.rs
@@ -11,8 +11,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct CreateOrUpdateComment {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: Note,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(default)]
   pub(crate) tag: Vec<Mention>,
diff --git a/crates/apub/src/protocol/activities/create_or_update/post.rs b/crates/apub/src/protocol/activities/create_or_update/post.rs
index db159c86..2061c881 100644
--- a/crates/apub/src/protocol/activities/create_or_update/post.rs
+++ b/crates/apub/src/protocol/activities/create_or_update/post.rs
@@ -10,8 +10,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct CreateOrUpdatePost {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: Page,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: CreateOrUpdateType,
diff --git a/crates/apub/src/protocol/activities/deletion/delete.rs b/crates/apub/src/protocol/activities/deletion/delete.rs
index 49adb6e6..a999e58e 100644
--- a/crates/apub/src/protocol/activities/deletion/delete.rs
+++ b/crates/apub/src/protocol/activities/deletion/delete.rs
@@ -13,6 +13,7 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct Delete {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: Tombstone,
   #[serde(rename = "type")]
diff --git a/crates/apub/src/protocol/activities/deletion/undo_delete.rs b/crates/apub/src/protocol/activities/deletion/undo_delete.rs
index d7608017..ebd51f96 100644
--- a/crates/apub/src/protocol/activities/deletion/undo_delete.rs
+++ b/crates/apub/src/protocol/activities/deletion/undo_delete.rs
@@ -11,8 +11,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct UndoDelete {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: Delete,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: UndoType,
diff --git a/crates/apub/src/protocol/activities/voting/undo_vote.rs b/crates/apub/src/protocol/activities/voting/undo_vote.rs
index 9a1767f2..d3f4fb33 100644
--- a/crates/apub/src/protocol/activities/voting/undo_vote.rs
+++ b/crates/apub/src/protocol/activities/voting/undo_vote.rs
@@ -11,8 +11,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct UndoVote {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: Vote,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: UndoType,
diff --git a/crates/apub/src/protocol/activities/voting/vote.rs b/crates/apub/src/protocol/activities/voting/vote.rs
index 065e3b31..ba5c629c 100644
--- a/crates/apub/src/protocol/activities/voting/vote.rs
+++ b/crates/apub/src/protocol/activities/voting/vote.rs
@@ -15,8 +15,10 @@ use url::Url;
 #[serde(rename_all = "camelCase")]
 pub struct Vote {
   pub(crate) actor: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   pub(crate) object: ObjectId<PostOrComment>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   #[serde(rename = "type")]
   pub(crate) kind: VoteType,
diff --git a/crates/apub/src/protocol/objects/note.rs b/crates/apub/src/protocol/objects/note.rs
index be21f8f5..39c606b0 100644
--- a/crates/apub/src/protocol/objects/note.rs
+++ b/crates/apub/src/protocol/objects/note.rs
@@ -23,8 +23,10 @@ pub struct Note {
   pub(crate) r#type: NoteType,
   pub(crate) id: ObjectId<ApubComment>,
   pub(crate) attributed_to: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   #[serde(default)]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   pub(crate) content: String,
   pub(crate) media_type: Option<MediaTypeHtml>,
diff --git a/crates/apub/src/protocol/objects/page.rs b/crates/apub/src/protocol/objects/page.rs
index 2bd5cc6d..1fca51e4 100644
--- a/crates/apub/src/protocol/objects/page.rs
+++ b/crates/apub/src/protocol/objects/page.rs
@@ -24,8 +24,10 @@ pub struct Page {
   pub(crate) r#type: PageType,
   pub(crate) id: ObjectId<ApubPost>,
   pub(crate) attributed_to: ObjectId<ApubPerson>,
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) to: Vec<Url>,
   #[serde(default)]
+  #[serde(deserialize_with = "crate::deserialize_one_or_many")]
   pub(crate) cc: Vec<Url>,
   pub(crate) name: String,
   pub(crate) content: Option<String>,
-- 
2.44.1