Skip to main content

syncweb_core/editorial/
channel.rs

1use serde::{Deserialize, Serialize};
2
3/// A named content stream for editorial distribution.
4///
5/// Channels provide **authoritative views** on top of the default,
6/// unfiltered gossip catalog.  Publishers announce content into channels
7/// once it reaches a given editorial state (e.g. "curated"), and consumers
8/// subscribe only to the channels they trust.
9///
10/// Under the hood a channel is a separate gossip topic derived from its
11/// name, so channel membership is inherently opt-in and decentralised.
12#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct Channel {
15    /// Human-readable channel name (e.g. `"curated"`, `"latest"`,
16    /// `"publisher-acme"`).
17    pub name: String,
18    /// Optional human-readable description.
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub description: Option<String>,
21    /// If non-empty, only announcements from these publishers are accepted.
22    ///
23    /// Each entry is a hex-encoded Ed25519 public key (with or without the
24    /// `ed25519:` prefix).
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    pub allowed_publishers: Vec<String>,
27}
28
29impl Channel {
30    /// Create a new channel with an optional description.
31    #[must_use]
32    pub fn new(name: impl Into<String>, description: Option<impl Into<String>>) -> Self {
33        Self {
34            name: name.into(),
35            description: description.map(Into::into),
36            allowed_publishers: Vec::new(),
37        }
38    }
39
40    /// Create a channel with an explicit publisher allowlist.
41    #[must_use]
42    pub fn with_publishers(
43        name: impl Into<String>,
44        description: Option<impl Into<String>>,
45        allowed_publishers: Vec<String>,
46    ) -> Self {
47        Self {
48            name: name.into(),
49            description: description.map(Into::into),
50            allowed_publishers,
51        }
52    }
53
54    /// The gossip topic seed for this channel.
55    ///
56    /// Topic IDs are derived from a BLAKE3 hash of this seed, so every
57    /// channel is a fully independent gossip swarm.
58    #[must_use]
59    pub fn topic_seed(&self) -> String {
60        format!("syncweb/editorial/{}/v1", self.name)
61    }
62
63    /// Whether content should appear on this channel.
64    ///
65    /// By default every channel accepts all content; publishers apply
66    /// editorial policy *before* calling announce.
67    #[must_use]
68    pub const fn accepts_all(&self) -> bool {
69        true
70    }
71}