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}