Skip to main content

max / makenotwork

11.5 KB · 304 lines History Blame Raw
1 //! The content policy at `/policy`, described.
2 //!
3 //! The third public document. It replaces `templates/pages/policy.html`,
4 //! `PolicyTemplate` and `landing::policy_page`.
5 //!
6 //! # Prose is prose, and rows are for data
7 //!
8 //! `/use-cases` set the rule that a card holding a structure is a region and a
9 //! card holding a sentence is a row. This page is the case that rule does not
10 //! reach: six sections of prose, with bullets that are sentences rather than
11 //! records, carrying inline links and one emphasised address.
12 //!
13 //! A `Row` cannot hold either. `Row::new("Report suspicious downloads to
14 //! reports@makenot.work")` loses the emphasis, and nothing in a row can carry
15 //! the link inside "part of our [creator guarantees]". Forcing prose through
16 //! rows would silently flatten both, and neither loss is visible in a test that
17 //! checks the text is present.
18 //!
19 //! So each section's body is one [`super::own_prose`], which is markdown and
20 //! renders through docengine -- the same path `/docs/*` takes, so the policy
21 //! prose and the documents it links to are formatted by one renderer. **What
22 //! stays described is the structure**: [`Node::section`] per heading, so the
23 //! section hierarchy is a fact of the screen rather than an `<h2>` inside a
24 //! blob.
25 //!
26 //! `own_prose` and not `Node::rich`, which is the difference between a page
27 //! that points at its own documents and a page that tells crawlers not to
28 //! follow them (quasi 0.94, quasicoherent `24a3b1df`).
29 //!
30 //! The escape hatch is not swallowing the page. It is carrying the one thing
31 //! this page is made of, which is sentences.
32 //!
33 //! # The exception, and it is the one list that is data
34 //!
35 //! "Other Policies" is seven links, each a title and a sentence saying what the
36 //! document covers. That is a record per row and a route per row, so it is a
37 //! [`Node::list`] of rows with acts, not markdown. The test is what it would
38 //! cost to add an eighth: a row, versus a line of prose somebody has to match
39 //! against six others by hand.
40
41 use makeover_layout as layout;
42 use quasi_router::screen::{Act, Row};
43 use quasi_router::{
44 Action, Document, Node, RegionKind, Request, Response, RouteError, Screen as Described, Slot,
45 };
46 use quasi_webview::Webview;
47
48 /// The address, registered whole. See [`super::public_document_mount`].
49 pub const PATH: &str = "/policy";
50
51 /// The page's own region, and what the skip link points at.
52 pub const PAGE_REGION: &str = "policy";
53
54 const MEASURE: layout::Measure = layout::Measure::Wide;
55
56 /// One headed section of the policy.
57 struct Section {
58 heading: &'static str,
59 /// The body, as markdown. See the module header for why this is prose
60 /// rather than nodes.
61 body: &'static str,
62 }
63
64 /// The five prose sections, in the template's order. "Other Policies" is not
65 /// here; see [`OTHER`].
66 const SECTIONS: &[Section] = &[
67 Section {
68 heading: "What's Welcome",
69 body: "Creative work across all supported types:\n\
70 \n\
71 - Software, plugins, presets, and templates\n\
72 - Audio: music, podcasts, samples, sound design\n\
73 - Writing: articles, guides, courses, fiction\n\
74 - Visual work: images, photography, design assets\n\
75 - Video: tutorials, performances, documentaries\n\
76 \n\
77 If you made it and it's legal to distribute, it belongs here.",
78 },
79 Section {
80 heading: "What's Not Allowed",
81 body: "- Content that violates applicable law\n\
82 - Harassment, threats, or doxxing\n\
83 - Spam, deceptive listings, or bait-and-switch pricing\n\
84 - Malware, exploits, or tools designed to cause harm\n\
85 - Impersonation of other creators or organizations\n\
86 - Content you don't have the rights to distribute",
87 },
88 Section {
89 heading: "How We Handle Issues",
90 body: "During private alpha, every creator has a direct relationship with the admin. \
91 If something comes up, we talk about it. No automated takedowns, no faceless \
92 tickets.\n\
93 \n\
94 Post-alpha, we'll introduce a formal process with written notice of any policy \
95 violation, an opportunity to appeal, and continued access to data export \
96 throughout.",
97 },
98 Section {
99 heading: "Your Rights",
100 body: "- Full data export is always available: your content, metadata, and transaction \
101 history\n\
102 - If we ever moderate content or suspend an account, you'll get a clear \
103 explanation of what policy was violated\n\
104 - You'll have the opportunity to appeal\n\
105 - You can export your data even while suspended (excluding content we can't \
106 legally host)\n\
107 \n\
108 These commitments are part of our [creator guarantees](/docs/guarantees).",
109 },
110 Section {
111 heading: "Software Downloads",
112 body: "Makenotwork hosts downloadable software uploaded by creators. While we take \
113 steps to make sure downloads are safe:\n\
114 \n\
115 - Creators are responsible for the safety and integrity of their uploads\n\
116 - Users should verify downloads with antivirus software before running them\n\
117 - We do not guarantee that any download is free of malware or other harmful \
118 content\n\
119 - Report suspicious downloads to **reports@makenot.work**",
120 },
121 ];
122
123 /// The other policy documents: a title, what it covers, and where it lives.
124 const OTHER: &[(&str, &str, &str)] = &[
125 (
126 "Terms of Service",
127 "What you agree to by using Makenotwork",
128 "/docs/terms-of-service",
129 ),
130 (
131 "Privacy Policy",
132 "What we collect, why, and how to exercise your rights",
133 "/docs/privacy-policy",
134 ),
135 (
136 "Payments & Refunds",
137 "Merchant-of-record model, refunds, chargebacks",
138 "/docs/payments",
139 ),
140 (
141 "Acceptable Use",
142 "Specific behaviour that gets accounts suspended",
143 "/docs/acceptable-use",
144 ),
145 (
146 "Copyright & DMCA",
147 "How takedowns and counter-notifications work",
148 "/docs/copyright",
149 ),
150 (
151 "Appeals",
152 "How to challenge a moderation decision",
153 "/docs/appeals",
154 ),
155 (
156 "Mailing List Data Processing",
157 "Who answers a subscriber's request about a project mailing list",
158 "/docs/mailing-list-data-processing",
159 ),
160 ];
161
162 /// The page. Reads nothing.
163 pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
164 Ok(page_screen().into())
165 }
166
167 /// The whole document: the title, the measure, the body.
168 fn page_screen() -> Described {
169 let mut page = Slot::new(PAGE_REGION, RegionKind::Pane)
170 .with(Node::page("Content Policy"))
171 .with(Node::text(
172 "Makenotwork exists so creators can sell their work on fair terms. This policy \
173 describes what belongs here, what doesn't, and how we handle problems.",
174 ));
175
176 for section in SECTIONS {
177 page = page
178 .with(Node::section(section.heading))
179 .with(super::own_prose(section.body));
180 }
181
182 page = page
183 .with(Node::section("Other Policies"))
184 .with(Node::list(OTHER.iter().map(|(title, covers, route)| {
185 Row::new(*title)
186 .secondary(*covers)
187 .act(Act::new("Read", Action::get(*route).navigating()))
188 })));
189
190 page = page.with(Node::section("Questions")).with(super::own_prose(
191 "If something's unclear or you want to check before posting, reach out at \
192 **policy@makenot.work**.",
193 ));
194
195 Described::single("Content Policy - Makenotwork")
196 .measured(MEASURE)
197 .documented(
198 Document::default().classed(crate::shell::body_class(MEASURE, &["policy-page"])),
199 )
200 .summarised("What belongs on Makenotwork, what doesn't, and how problems are handled.")
201 .with(page)
202 }
203
204 /// The document this screen is drawn in. Same shape as the other public
205 /// documents: the skip link and the site header, whose user is optional here.
206 #[must_use]
207 pub fn renderer(viewer: &super::Viewer) -> Webview {
208 Webview::new().with_shell(viewer.document_shell().with_body_first(format!(
209 "{}{}",
210 crate::shell::skip_link(PAGE_REGION),
211 crate::shell::site_header(viewer.user.as_ref()),
212 )))
213 }
214
215 #[cfg(test)]
216 mod tests {
217 use super::*;
218
219 fn html() -> String {
220 use quasi_axum::Serves as _;
221
222 Webview::new().screen(&page_screen())
223 }
224
225 /// `2790e5c4`. This template carried both classes on the body already, so
226 /// this one is a copy rather than the merge `/team` and `/use-cases` were.
227 #[test]
228 fn the_document_carries_the_classes_the_template_carried() {
229 let screen = page_screen();
230
231 assert_eq!(
232 screen.document.body_class.as_deref(),
233 Some("padded-page policy-page")
234 );
235 assert!(
236 html().contains("class=\"padded-page policy-page\""),
237 "{}",
238 html()
239 );
240 }
241
242 /// The two things rows would have flattened, which is the whole argument
243 /// for prose being prose: an inline link inside a sentence, and an
244 /// emphasised address inside a bullet.
245 #[test]
246 fn the_prose_keeps_its_inline_link_and_its_emphasis() {
247 let html = html();
248
249 assert!(
250 html.contains(r#"href="/docs/guarantees""#),
251 "the creator-guarantees link did not survive: {html}"
252 );
253 // This page exists to point at the other policy documents, so a
254 // `nofollow` on the way there would be the page working against itself.
255 // The seal on `super::own_prose`: an untrusted source is hardened.
256 assert!(
257 !html.contains("nofollow"),
258 "the policy page nofollowed its own documents: {html}"
259 );
260 assert!(
261 html.contains("<strong>reports@makenot.work</strong>")
262 || html.contains("<b>reports@makenot.work</b>"),
263 "the reports address lost its emphasis: {html}"
264 );
265 }
266
267 /// Every policy document the page pointed at is still pointed at.
268 ///
269 /// The titles are compared escaped, because that is what lands in the
270 /// markup: two of the seven carry an ampersand, and the description layer
271 /// escapes it exactly as the template's `&amp;` did.
272 #[test]
273 fn every_other_policy_keeps_its_row_and_its_route() {
274 let html = html();
275
276 for (title, _, route) in OTHER {
277 let escaped = crate::helpers::escape_html(title);
278 assert!(html.contains(&escaped), "{title} missing");
279 assert!(html.contains(route), "{route} missing");
280 }
281 }
282
283 /// Both addresses a reader is told to write to are still on the page. They
284 /// are different mailboxes on purpose (`feedback_mnw_email_routing`), so a
285 /// conversion that collapsed them would be a real loss.
286 #[test]
287 fn both_contact_addresses_survive_and_stay_distinct() {
288 let html = html();
289
290 assert!(html.contains("reports@makenot.work"), "{html}");
291 assert!(html.contains("policy@makenot.work"), "{html}");
292 }
293
294 /// `736f45a5`: this screen's markup carries none of the four spellings.
295 #[test]
296 fn the_page_spells_no_spinner() {
297 let html = html();
298
299 for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] {
300 assert!(!html.contains(spelling), "{spelling} survives in {html}");
301 }
302 }
303 }
304