| 126 |
126 |
|
}
|
| 127 |
127 |
|
}
|
| 128 |
128 |
|
|
|
129 |
+ |
// ChatPolicy, per-community chat read/write policy
|
|
130 |
+ |
|
|
131 |
+ |
/// Per-community chat policy (`communities.chat_policy`).
|
|
132 |
+ |
///
|
|
133 |
+ |
/// Read access here is on top of what the community already allows; chat never
|
|
134 |
+ |
/// widens forum access. Write access is always additionally gated by
|
|
135 |
+ |
/// `check_write_access` (not suspended, not banned, not muted) and by
|
|
136 |
+ |
/// [`CommunityState`], which puts `Frozen` and `Archived` communities in
|
|
137 |
+ |
/// read-only regardless of the policy.
|
|
138 |
+ |
///
|
|
139 |
+ |
/// - `Off`: no route, no hub room, no UI affordance. Nobody reads or writes.
|
|
140 |
+ |
/// - `Members`: anyone who can view the forum reads; members write.
|
|
141 |
+ |
/// - `PublicRead`: anyone reads, including logged out; members write.
|
|
142 |
+ |
/// - `FanPlus`: anyone who can view the forum reads; Fan+ writes.
|
|
143 |
+ |
///
|
|
144 |
+ |
/// The default is `Off`, including for communities that predate the column.
|
|
145 |
+ |
/// Chat is a live moderation burden, so an owner opts into carrying it.
|
|
146 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, sqlx::Type)]
|
|
147 |
+ |
#[sqlx(type_name = "TEXT", rename_all = "snake_case")]
|
|
148 |
+ |
pub enum ChatPolicy {
|
|
149 |
+ |
#[default]
|
|
150 |
+ |
Off,
|
|
151 |
+ |
Members,
|
|
152 |
+ |
PublicRead,
|
|
153 |
+ |
FanPlus,
|
|
154 |
+ |
}
|
|
155 |
+ |
|
|
156 |
+ |
impl Serialize for ChatPolicy {
|
|
157 |
+ |
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
158 |
+ |
serializer.serialize_str(self.as_str())
|
|
159 |
+ |
}
|
|
160 |
+ |
}
|
|
161 |
+ |
|
|
162 |
+ |
impl ChatPolicy {
|
|
163 |
+ |
/// Every policy, in the order the owner settings UI offers them.
|
|
164 |
+ |
pub const ALL: [Self; 4] = [Self::Off, Self::Members, Self::PublicRead, Self::FanPlus];
|
|
165 |
+ |
|
|
166 |
+ |
pub fn from_db(s: &str) -> Option<Self> {
|
|
167 |
+ |
match s {
|
|
168 |
+ |
"off" => Some(Self::Off),
|
|
169 |
+ |
"members" => Some(Self::Members),
|
|
170 |
+ |
"public_read" => Some(Self::PublicRead),
|
|
171 |
+ |
"fan_plus" => Some(Self::FanPlus),
|
|
172 |
+ |
_ => None,
|
|
173 |
+ |
}
|
|
174 |
+ |
}
|
|
175 |
+ |
|
|
176 |
+ |
pub fn as_str(self) -> &'static str {
|
|
177 |
+ |
match self {
|
|
178 |
+ |
Self::Off => "off",
|
|
179 |
+ |
Self::Members => "members",
|
|
180 |
+ |
Self::PublicRead => "public_read",
|
|
181 |
+ |
Self::FanPlus => "fan_plus",
|
|
182 |
+ |
}
|
|
183 |
+ |
}
|
|
184 |
+ |
|
|
185 |
+ |
/// Whether chat exists at all for this community. `Off` must be total: the
|
|
186 |
+ |
/// route 404s, no hub room is created, and no affordance renders.
|
|
187 |
+ |
pub fn is_enabled(self) -> bool {
|
|
188 |
+ |
!matches!(self, Self::Off)
|
|
189 |
+ |
}
|
|
190 |
+ |
|
|
191 |
+ |
/// Whether a logged-out visitor may read the room.
|
|
192 |
+ |
pub fn allows_logged_out_read(self) -> bool {
|
|
193 |
+ |
matches!(self, Self::PublicRead)
|
|
194 |
+ |
}
|
|
195 |
+ |
|
|
196 |
+ |
/// Whether writing requires `UserPerks::effective_plus()` on top of the
|
|
197 |
+ |
/// usual good-standing check.
|
|
198 |
+ |
pub fn requires_fan_plus_to_write(self) -> bool {
|
|
199 |
+ |
matches!(self, Self::FanPlus)
|
|
200 |
+ |
}
|
|
201 |
+ |
}
|
|
202 |
+ |
|
|
203 |
+ |
impl fmt::Display for ChatPolicy {
|
|
204 |
+ |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
205 |
+ |
f.write_str(self.as_str())
|
|
206 |
+ |
}
|
|
207 |
+ |
}
|
|
208 |
+ |
|
| 129 |
209 |
|
// BanType, ban or mute
|
| 130 |
210 |
|
|
| 131 |
211 |
|
/// Type of community restriction.
|
| 375 |
455 |
|
assert!(!CommunityState::Active.is_hidden_from_default_listing());
|
| 376 |
456 |
|
}
|
| 377 |
457 |
|
|
|
458 |
+ |
#[test]
|
|
459 |
+ |
fn chat_policy_roundtrip() {
|
|
460 |
+ |
for p in ChatPolicy::ALL {
|
|
461 |
+ |
assert_eq!(ChatPolicy::from_db(p.as_str()), Some(p));
|
|
462 |
+ |
}
|
|
463 |
+ |
assert_eq!(ChatPolicy::from_db("bogus"), None);
|
|
464 |
+ |
// The stored strings are the migration's CHECK values, snake_case.
|
|
465 |
+ |
assert_eq!(ChatPolicy::PublicRead.as_str(), "public_read");
|
|
466 |
+ |
assert_eq!(ChatPolicy::FanPlus.as_str(), "fan_plus");
|
|
467 |
+ |
}
|
|
468 |
+ |
|
|
469 |
+ |
#[test]
|
|
470 |
+ |
fn chat_policy_defaults_to_off() {
|
|
471 |
+ |
assert_eq!(ChatPolicy::default(), ChatPolicy::Off);
|
|
472 |
+ |
assert!(!ChatPolicy::Off.is_enabled());
|
|
473 |
+ |
}
|
|
474 |
+ |
|
|
475 |
+ |
#[test]
|
|
476 |
+ |
fn chat_policy_predicates() {
|
|
477 |
+ |
// Only public_read is readable logged out.
|
|
478 |
+ |
assert!(ChatPolicy::PublicRead.allows_logged_out_read());
|
|
479 |
+ |
for p in [ChatPolicy::Off, ChatPolicy::Members, ChatPolicy::FanPlus] {
|
|
480 |
+ |
assert!(!p.allows_logged_out_read());
|
|
481 |
+ |
}
|
|
482 |
+ |
|
|
483 |
+ |
// Only fan_plus adds a tier requirement on top of good standing.
|
|
484 |
+ |
assert!(ChatPolicy::FanPlus.requires_fan_plus_to_write());
|
|
485 |
+ |
for p in [ChatPolicy::Off, ChatPolicy::Members, ChatPolicy::PublicRead] {
|
|
486 |
+ |
assert!(!p.requires_fan_plus_to_write());
|
|
487 |
+ |
}
|
|
488 |
+ |
|
|
489 |
+ |
// Every mode but off is a live room.
|
|
490 |
+ |
assert!(ChatPolicy::Members.is_enabled());
|
|
491 |
+ |
assert!(ChatPolicy::PublicRead.is_enabled());
|
|
492 |
+ |
assert!(ChatPolicy::FanPlus.is_enabled());
|
|
493 |
+ |
}
|
|
494 |
+ |
|
| 378 |
495 |
|
#[test]
|
| 379 |
496 |
|
fn mod_action_display() {
|
| 380 |
497 |
|
assert_eq!(ModAction::PinThread.as_str(), "pin_thread");
|