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