@geekity/cms 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +292 -45
- package/admin/pages/comments/all.njk +3 -0
- package/admin/pages/settings/discussion.njk +53 -0
- package/admin/pages/settings/email.njk +20 -0
- package/admin/pages/tools/personal-data.njk +67 -0
- package/admin/pages/users/edit.njk +1 -1
- package/dist/admin/comments.d.ts +5 -0
- package/dist/admin/comments.d.ts.map +1 -1
- package/dist/admin/comments.js +13 -0
- package/dist/admin/comments.js.map +1 -1
- package/dist/admin/headers.d.ts +14 -5
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +18 -6
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/menu.d.ts.map +1 -1
- package/dist/admin/menu.js +5 -1
- package/dist/admin/menu.js.map +1 -1
- package/dist/admin/personal-data.d.ts +35 -0
- package/dist/admin/personal-data.d.ts.map +1 -0
- package/dist/admin/personal-data.js +90 -0
- package/dist/admin/personal-data.js.map +1 -0
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +18 -2
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/session.d.ts +42 -1
- package/dist/admin/session.d.ts.map +1 -1
- package/dist/admin/session.js +74 -4
- package/dist/admin/session.js.map +1 -1
- package/dist/admin/settings-discussion.d.ts.map +1 -1
- package/dist/admin/settings-discussion.js +9 -1
- package/dist/admin/settings-discussion.js.map +1 -1
- package/dist/admin/settings-email.d.ts.map +1 -1
- package/dist/admin/settings-email.js +10 -1
- package/dist/admin/settings-email.js.map +1 -1
- package/dist/admin/settings.d.ts +39 -0
- package/dist/admin/settings.d.ts.map +1 -1
- package/dist/admin/settings.js +144 -0
- package/dist/admin/settings.js.map +1 -1
- package/dist/admin/store.d.ts +30 -0
- package/dist/admin/store.d.ts.map +1 -1
- package/dist/admin/store.js +68 -5
- package/dist/admin/store.js.map +1 -1
- package/dist/admin/templates.d.ts +1 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +1 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/avatars/avatars.d.ts +112 -0
- package/dist/avatars/avatars.d.ts.map +1 -0
- package/dist/avatars/avatars.js +259 -0
- package/dist/avatars/avatars.js.map +1 -0
- package/dist/avatars/index.d.ts +4 -0
- package/dist/avatars/index.d.ts.map +1 -0
- package/dist/avatars/index.js +3 -0
- package/dist/avatars/index.js.map +1 -0
- package/dist/avatars/routes.d.ts +11 -0
- package/dist/avatars/routes.d.ts.map +1 -0
- package/dist/avatars/routes.js +33 -0
- package/dist/avatars/routes.js.map +1 -0
- package/dist/comments/records.d.ts +15 -0
- package/dist/comments/records.d.ts.map +1 -1
- package/dist/comments/records.js +51 -1
- package/dist/comments/records.js.map +1 -1
- package/dist/comments/routes.d.ts.map +1 -1
- package/dist/comments/routes.js +7 -1
- package/dist/comments/routes.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +57 -0
- package/dist/config.js.map +1 -1
- package/dist/contact/index.d.ts +1 -1
- package/dist/contact/index.d.ts.map +1 -1
- package/dist/contact/index.js +1 -1
- package/dist/contact/index.js.map +1 -1
- package/dist/contact/records.d.ts +8 -2
- package/dist/contact/records.d.ts.map +1 -1
- package/dist/contact/records.js +13 -6
- package/dist/contact/records.js.map +1 -1
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/quotes.d.ts +2 -0
- package/dist/federation/quotes.d.ts.map +1 -1
- package/dist/federation/quotes.js +4 -0
- package/dist/federation/quotes.js.map +1 -1
- package/dist/federation/replies.d.ts +23 -0
- package/dist/federation/replies.d.ts.map +1 -1
- package/dist/federation/replies.js +47 -10
- package/dist/federation/replies.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +41 -7
- package/dist/index.js.map +1 -1
- package/dist/notifications/index.d.ts +1 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +1 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/optouts.d.ts +6 -0
- package/dist/notifications/optouts.d.ts.map +1 -1
- package/dist/notifications/optouts.js +17 -0
- package/dist/notifications/optouts.js.map +1 -1
- package/dist/privacy/erase.d.ts +45 -0
- package/dist/privacy/erase.d.ts.map +1 -0
- package/dist/privacy/erase.js +77 -0
- package/dist/privacy/erase.js.map +1 -0
- package/dist/privacy/policy.d.ts +32 -0
- package/dist/privacy/policy.d.ts.map +1 -0
- package/dist/privacy/policy.js +37 -0
- package/dist/privacy/policy.js.map +1 -0
- package/dist/privacy/retention.d.ts +79 -0
- package/dist/privacy/retention.d.ts.map +1 -0
- package/dist/privacy/retention.js +133 -0
- package/dist/privacy/retention.js.map +1 -0
- package/dist/web/conversation.d.ts +9 -1
- package/dist/web/conversation.d.ts.map +1 -1
- package/dist/web/conversation.js +68 -7
- package/dist/web/conversation.js.map +1 -1
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +1 -0
- package/dist/web/index.js.map +1 -1
- package/dist/web/maintenance.d.ts.map +1 -1
- package/dist/web/maintenance.js +7 -2
- package/dist/web/maintenance.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +4 -0
- package/dist/web/routes.js.map +1 -1
- package/dist/web/well-known.d.ts +21 -0
- package/dist/web/well-known.d.ts.map +1 -0
- package/dist/web/well-known.js +66 -0
- package/dist/web/well-known.js.map +1 -0
- package/dist/webmention/fetch-public.d.ts +34 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -0
- package/dist/webmention/fetch-public.js +101 -0
- package/dist/webmention/fetch-public.js.map +1 -0
- package/dist/webmention/reply-context.d.ts.map +1 -1
- package/dist/webmention/reply-context.js +12 -89
- package/dist/webmention/reply-context.js.map +1 -1
- package/package.json +1 -1
- package/templates/site/content/_data/site.json +7 -1
- package/templates/site/content/pages/privacy.md +127 -0
- package/themes/default/README.md +38 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optouts.js","sourceRoot":"","sources":["../../src/notifications/optouts.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"optouts.js","sourceRoot":"","sources":["../../src/notifications/optouts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CommentRecord } from '../admin/store.ts';
|
|
2
|
+
import type { CommentRecords } from '../comments/records.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Erasing one person's personal data when they ask (TASK-135).
|
|
5
|
+
*
|
|
6
|
+
* A person is found by the email they gave, because that is the one thing
|
|
7
|
+
* they can prove is theirs: a name is anybody's to type. Their comments stay
|
|
8
|
+
* where they are in the thread, with their status and their words, under the
|
|
9
|
+
* name {@link ERASED_NAME} and with nothing else of theirs left on them; a
|
|
10
|
+
* comment deleted outright would take every reply to it out of the thread,
|
|
11
|
+
* and the Comments screen is there for a comment whose words have to go too.
|
|
12
|
+
* Their contact messages are deleted, because a message is nothing but what
|
|
13
|
+
* they wrote and where to answer them, and the opt-out list forgets their
|
|
14
|
+
* address.
|
|
15
|
+
*/
|
|
16
|
+
/** What an erased comment is signed with. */
|
|
17
|
+
export declare const ERASED_NAME = "Anonymous";
|
|
18
|
+
/** What was found for an address, or what was erased for it. */
|
|
19
|
+
export interface PersonalDataReport {
|
|
20
|
+
/** Comments carrying the address. */
|
|
21
|
+
readonly comments: number;
|
|
22
|
+
/** Contact messages sent from it. */
|
|
23
|
+
readonly messages: number;
|
|
24
|
+
/** Whether it is on the list of addresses not to email about replies. */
|
|
25
|
+
readonly optedOut: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Where a person's data can be. */
|
|
28
|
+
export interface PersonalDataStores {
|
|
29
|
+
/** The comment files and the index over them. */
|
|
30
|
+
readonly records: CommentRecords;
|
|
31
|
+
/** The data directory the contact messages and the opt-out list live in. */
|
|
32
|
+
readonly dataDir: string;
|
|
33
|
+
}
|
|
34
|
+
/** A comment with everything that says who wrote it taken off. */
|
|
35
|
+
export declare function eraseCommentAuthor(comment: CommentRecord): CommentRecord;
|
|
36
|
+
/** What the site holds under `email`, without changing any of it. */
|
|
37
|
+
export declare function findPersonalData(stores: PersonalDataStores, email: string): PersonalDataReport;
|
|
38
|
+
/**
|
|
39
|
+
* Erase everything the site holds under `email`, and say what was erased.
|
|
40
|
+
*
|
|
41
|
+
* Both this and {@link findPersonalData} read the files rather than the index,
|
|
42
|
+
* so a comment written by hand since the last boot is counted and erased.
|
|
43
|
+
*/
|
|
44
|
+
export declare function erasePersonalData(stores: PersonalDataStores, email: string): Promise<PersonalDataReport>;
|
|
45
|
+
//# sourceMappingURL=erase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erase.d.ts","sourceRoot":"","sources":["../../src/privacy/erase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D;;;;;;;;;;;;GAYG;AAEH,6CAA6C;AAC7C,eAAO,MAAM,WAAW,cAAc,CAAC;AAEvC,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,oCAAoC;AACpC,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAYD,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,aAAa,CAYxE;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAU9F;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,kBAAkB,CAAC,CAoB7B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { commentSlugs, readComments, rewriteComments } from '../comments/records.js';
|
|
2
|
+
import { deleteContactMessage, listContactMessages } from '../contact/records.js';
|
|
3
|
+
import { hasOptedOut, removeCommentOptOut } from '../notifications/optouts.js';
|
|
4
|
+
import { withRedacted } from './retention.js';
|
|
5
|
+
/**
|
|
6
|
+
* Erasing one person's personal data when they ask (TASK-135).
|
|
7
|
+
*
|
|
8
|
+
* A person is found by the email they gave, because that is the one thing
|
|
9
|
+
* they can prove is theirs: a name is anybody's to type. Their comments stay
|
|
10
|
+
* where they are in the thread, with their status and their words, under the
|
|
11
|
+
* name {@link ERASED_NAME} and with nothing else of theirs left on them; a
|
|
12
|
+
* comment deleted outright would take every reply to it out of the thread,
|
|
13
|
+
* and the Comments screen is there for a comment whose words have to go too.
|
|
14
|
+
* Their contact messages are deleted, because a message is nothing but what
|
|
15
|
+
* they wrote and where to answer them, and the opt-out list forgets their
|
|
16
|
+
* address.
|
|
17
|
+
*/
|
|
18
|
+
/** What an erased comment is signed with. */
|
|
19
|
+
export const ERASED_NAME = 'Anonymous';
|
|
20
|
+
/** An address as it is compared: trimmed and folded, as the opt-out list keeps it. */
|
|
21
|
+
function folded(email) {
|
|
22
|
+
return email.trim().toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
/** Whether a comment was left under `email`. */
|
|
25
|
+
function isTheirs(comment, email) {
|
|
26
|
+
return comment.author.email !== null && folded(comment.author.email) === email;
|
|
27
|
+
}
|
|
28
|
+
/** A comment with everything that says who wrote it taken off. */
|
|
29
|
+
export function eraseCommentAuthor(comment) {
|
|
30
|
+
return {
|
|
31
|
+
...comment,
|
|
32
|
+
author: { name: ERASED_NAME, url: null, email: null, avatar: null },
|
|
33
|
+
addressHash: null,
|
|
34
|
+
notify: false,
|
|
35
|
+
redacted: withRedacted(comment.redacted, [
|
|
36
|
+
'email',
|
|
37
|
+
...(comment.addressHash === null ? [] : ['addressHash']),
|
|
38
|
+
'author',
|
|
39
|
+
]),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** What the site holds under `email`, without changing any of it. */
|
|
43
|
+
export function findPersonalData(stores, email) {
|
|
44
|
+
const wanted = folded(email);
|
|
45
|
+
const { contentDir } = stores.records;
|
|
46
|
+
const comments = commentSlugs(contentDir)
|
|
47
|
+
.flatMap((slug) => readComments(contentDir, slug))
|
|
48
|
+
.filter((comment) => isTheirs(comment, wanted)).length;
|
|
49
|
+
const messages = listContactMessages(stores.dataDir).filter((message) => folded(message.from.email) === wanted).length;
|
|
50
|
+
return { comments, messages, optedOut: hasOptedOut(stores.dataDir, wanted) };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Erase everything the site holds under `email`, and say what was erased.
|
|
54
|
+
*
|
|
55
|
+
* Both this and {@link findPersonalData} read the files rather than the index,
|
|
56
|
+
* so a comment written by hand since the last boot is counted and erased.
|
|
57
|
+
*/
|
|
58
|
+
export async function erasePersonalData(stores, email) {
|
|
59
|
+
const wanted = folded(email);
|
|
60
|
+
if (wanted === '')
|
|
61
|
+
return { comments: 0, messages: 0, optedOut: false };
|
|
62
|
+
let comments = 0;
|
|
63
|
+
for (const slug of commentSlugs(stores.records.contentDir)) {
|
|
64
|
+
const changed = await rewriteComments(stores.records, slug, (comment) => isTheirs(comment, wanted) ? eraseCommentAuthor(comment) : undefined);
|
|
65
|
+
comments += changed.length;
|
|
66
|
+
}
|
|
67
|
+
let messages = 0;
|
|
68
|
+
for (const message of listContactMessages(stores.dataDir)) {
|
|
69
|
+
if (folded(message.from.email) !== wanted)
|
|
70
|
+
continue;
|
|
71
|
+
if (await deleteContactMessage(stores.dataDir, message.id))
|
|
72
|
+
messages += 1;
|
|
73
|
+
}
|
|
74
|
+
const optedOut = await removeCommentOptOut(stores.dataDir, wanted);
|
|
75
|
+
return { comments, messages, optedOut };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=erase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erase.js","sourceRoot":"","sources":["../../src/privacy/erase.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long this site keeps the personal data its readers hand it (TASK-135).
|
|
3
|
+
*
|
|
4
|
+
* Three periods, each in whole days counted from the moment the data arrived,
|
|
5
|
+
* and `0` for keeping it for as long as the site stands. They are settings in
|
|
6
|
+
* `content/_data/site.json` rather than config: how long a site keeps what
|
|
7
|
+
* people give it is the owner's decision, made on the Discussion screen, and a
|
|
8
|
+
* public file is the right place for a policy a privacy notice will quote.
|
|
9
|
+
*/
|
|
10
|
+
/** Days a commenter's email is kept: long enough for a reply to reach them. */
|
|
11
|
+
export declare const RECOMMENDED_COMMENT_EMAIL_RETENTION_DAYS = 180;
|
|
12
|
+
/** Days an address hash is kept: long enough to spot a run of spam. */
|
|
13
|
+
export declare const RECOMMENDED_ADDRESS_HASH_RETENTION_DAYS = 30;
|
|
14
|
+
/** Days a contact message is kept: a year of correspondence. */
|
|
15
|
+
export declare const RECOMMENDED_CONTACT_MESSAGE_RETENTION_DAYS = 365;
|
|
16
|
+
/** The three periods. Zero keeps that data forever. */
|
|
17
|
+
export interface RetentionPolicy {
|
|
18
|
+
/** Days a comment keeps its author's email, and with it their reply subscription. */
|
|
19
|
+
readonly commentEmailDays: number;
|
|
20
|
+
/** Days a comment or a contact message keeps the hash of the address it came from. */
|
|
21
|
+
readonly addressHashDays: number;
|
|
22
|
+
/** Days a contact message is kept at all. */
|
|
23
|
+
readonly contactMessageDays: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether data that arrived at `at` has outlived a period of `days`.
|
|
27
|
+
*
|
|
28
|
+
* A date that will not parse has no age, so it is never past anything: a hand
|
|
29
|
+
* edit that lost a timestamp should not be what deletes somebody's message.
|
|
30
|
+
*/
|
|
31
|
+
export declare function outlived(at: string, days: number, now: Date): boolean;
|
|
32
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/privacy/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,+EAA+E;AAC/E,eAAO,MAAM,wCAAwC,MAAM,CAAC;AAE5D,uEAAuE;AACvE,eAAO,MAAM,uCAAuC,KAAK,CAAC;AAE1D,gEAAgE;AAChE,eAAO,MAAM,0CAA0C,MAAM,CAAC;AAE9D,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,sFAAsF;IACtF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,6CAA6C;IAC7C,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAID;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAKrE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long this site keeps the personal data its readers hand it (TASK-135).
|
|
3
|
+
*
|
|
4
|
+
* Three periods, each in whole days counted from the moment the data arrived,
|
|
5
|
+
* and `0` for keeping it for as long as the site stands. They are settings in
|
|
6
|
+
* `content/_data/site.json` rather than config: how long a site keeps what
|
|
7
|
+
* people give it is the owner's decision, made on the Discussion screen, and a
|
|
8
|
+
* public file is the right place for a policy a privacy notice will quote.
|
|
9
|
+
*/
|
|
10
|
+
/*
|
|
11
|
+
* A site that never chose a period keeps everything: a `site.json` without the
|
|
12
|
+
* keys reads as `0`, so upgrading the package never deletes anything. The
|
|
13
|
+
* periods below are what `geekity init` writes for a new site and what the
|
|
14
|
+
* Discussion screen recommends to an existing one.
|
|
15
|
+
*/
|
|
16
|
+
/** Days a commenter's email is kept: long enough for a reply to reach them. */
|
|
17
|
+
export const RECOMMENDED_COMMENT_EMAIL_RETENTION_DAYS = 180;
|
|
18
|
+
/** Days an address hash is kept: long enough to spot a run of spam. */
|
|
19
|
+
export const RECOMMENDED_ADDRESS_HASH_RETENTION_DAYS = 30;
|
|
20
|
+
/** Days a contact message is kept: a year of correspondence. */
|
|
21
|
+
export const RECOMMENDED_CONTACT_MESSAGE_RETENTION_DAYS = 365;
|
|
22
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
23
|
+
/**
|
|
24
|
+
* Whether data that arrived at `at` has outlived a period of `days`.
|
|
25
|
+
*
|
|
26
|
+
* A date that will not parse has no age, so it is never past anything: a hand
|
|
27
|
+
* edit that lost a timestamp should not be what deletes somebody's message.
|
|
28
|
+
*/
|
|
29
|
+
export function outlived(at, days, now) {
|
|
30
|
+
if (days <= 0)
|
|
31
|
+
return false;
|
|
32
|
+
const arrived = Date.parse(at);
|
|
33
|
+
if (Number.isNaN(arrived))
|
|
34
|
+
return false;
|
|
35
|
+
return now.getTime() - arrived > days * DAY_MS;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/privacy/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;GAKG;AAEH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,wCAAwC,GAAG,GAAG,CAAC;AAE5D,uEAAuE;AACvE,MAAM,CAAC,MAAM,uCAAuC,GAAG,EAAE,CAAC;AAE1D,gEAAgE;AAChE,MAAM,CAAC,MAAM,0CAA0C,GAAG,GAAG,CAAC;AAY9D,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAU,EAAE,IAAY,EAAE,GAAS;IAC1D,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AdminStore, CommentRecord, RedactedField } from '../admin/store.ts';
|
|
2
|
+
import type { ResolvedConfig } from '../config.ts';
|
|
3
|
+
import type { ContactMessage } from '../contact/records.ts';
|
|
4
|
+
import type { NotificationTimers } from '../notifications/digest.ts';
|
|
5
|
+
import type { RetentionPolicy } from './policy.ts';
|
|
6
|
+
/**
|
|
7
|
+
* The retention sweep (TASK-135): what removes the personal data a site has
|
|
8
|
+
* kept for longer than it said it would.
|
|
9
|
+
*
|
|
10
|
+
* The files are the truth (decision-9), so the sweep reads them rather than
|
|
11
|
+
* the index: every comment file under `content/_data/comments/` and every
|
|
12
|
+
* message under `data/contact/`, each rewritten inside the lock every other
|
|
13
|
+
* writer of that file takes, and each comment it changes put back into the
|
|
14
|
+
* index in the same step. Removing a value that is already gone is not a
|
|
15
|
+
* change, so a sweep run twice, or run again after a crash halfway through,
|
|
16
|
+
* finishes in the same place and writes nothing the second time.
|
|
17
|
+
*/
|
|
18
|
+
/** How often the sweep runs once the site serves. */
|
|
19
|
+
export declare const RETENTION_SWEEP_MS: number;
|
|
20
|
+
/** What one sweep changed. */
|
|
21
|
+
export interface RetentionReport {
|
|
22
|
+
/** Comments that lost an email, an address hash or both. */
|
|
23
|
+
readonly comments: number;
|
|
24
|
+
/** Contact messages deleted for being older than the site keeps them. */
|
|
25
|
+
readonly messagesDeleted: number;
|
|
26
|
+
/** Contact messages that lost their address hash. */
|
|
27
|
+
readonly messagesRedacted: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* One comment as the policy leaves it, or `undefined` when it has nothing to
|
|
31
|
+
* give up. The email goes with the reply subscription it served, since there
|
|
32
|
+
* is nowhere left to send one; the thread, the status and the words stay.
|
|
33
|
+
*/
|
|
34
|
+
export declare function expireComment(comment: CommentRecord, policy: RetentionPolicy, now: Date): CommentRecord | undefined;
|
|
35
|
+
/** What a contact message comes to under the policy. */
|
|
36
|
+
export type ContactExpiry = {
|
|
37
|
+
readonly kind: 'keep';
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'delete';
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: 'redact';
|
|
42
|
+
readonly message: ContactMessage;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* One contact message under the policy: kept as it is, deleted outright, or
|
|
46
|
+
* kept without the hash of the address it came from. A message is its
|
|
47
|
+
* sender's words and address together, so it has no smaller form to shrink
|
|
48
|
+
* to once its own period is up.
|
|
49
|
+
*/
|
|
50
|
+
export declare function expireContactMessage(message: ContactMessage, policy: RetentionPolicy, now: Date): ContactExpiry;
|
|
51
|
+
/** `held` with `added` joined in, in {@link REDACTED_FIELDS} order. */
|
|
52
|
+
export declare function withRedacted(held: readonly RedactedField[] | undefined, added: readonly RedactedField[]): RedactedField[];
|
|
53
|
+
/** What {@link createRetentionService} needs. */
|
|
54
|
+
export interface CreateRetentionServiceOptions {
|
|
55
|
+
/** The comment index the sweep keeps in step with the files. */
|
|
56
|
+
readonly admin: AdminStore;
|
|
57
|
+
/** Where the files are, and the clock the ages are measured by. */
|
|
58
|
+
readonly config: Pick<ResolvedConfig, 'contentDir' | 'dataDir' | 'now'>;
|
|
59
|
+
/** Defaults to the real interval timer. */
|
|
60
|
+
readonly timers?: NotificationTimers | undefined;
|
|
61
|
+
/** Defaults to `console`. */
|
|
62
|
+
readonly logger?: {
|
|
63
|
+
warn(message: string): void;
|
|
64
|
+
} | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** The sweep, on a timer once the site serves. */
|
|
67
|
+
export interface RetentionService {
|
|
68
|
+
/** Sweep now, queued behind any sweep already running, and say what changed. */
|
|
69
|
+
sweep(): Promise<RetentionReport>;
|
|
70
|
+
/** Sweep now and then every {@link RETENTION_SWEEP_MS}. Safe twice. */
|
|
71
|
+
start(): void;
|
|
72
|
+
/** Stop the timer. Safe before starting and safe twice. */
|
|
73
|
+
stop(): void;
|
|
74
|
+
/** Resolve once the sweep in flight has finished. */
|
|
75
|
+
settled(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** Build the retention sweep for one site. */
|
|
78
|
+
export declare function createRetentionService(options: CreateRetentionServiceOptions): RetentionService;
|
|
79
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/privacy/retention.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAIlF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;;;;;;;GAWG;AAEH,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,QAAqB,CAAC;AAErD,8BAA8B;AAC9B,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,qDAAqD;IACrD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,IAAI,GACR,aAAa,GAAG,SAAS,CAiB3B;AAED,wDAAwD;AACxD,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC;AAElE;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,IAAI,GACR,aAAa,CAMf;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EAC1C,KAAK,EAAE,SAAS,aAAa,EAAE,GAC9B,aAAa,EAAE,CAGjB;AAED,iDAAiD;AACjD,MAAM,WAAW,6BAA6B;IAC5C,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC,CAAC;IACxE,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACjD,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS,CAAC;CAC/D;AAED,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IAClC,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAAC;IACd,2DAA2D;IAC3D,IAAI,IAAI,IAAI,CAAC;IACb,qDAAqD;IACrD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAID,8CAA8C;AAC9C,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,6BAA6B,GAAG,gBAAgB,CAsE/F"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { REDACTED_FIELDS } from '../admin/store.js';
|
|
2
|
+
import { readSiteSettings, retentionPolicyOf } from '../admin/settings.js';
|
|
3
|
+
import { commentSlugs, rewriteComments } from '../comments/records.js';
|
|
4
|
+
import { deleteContactMessage, listContactMessages, updateContactMessage, } from '../contact/records.js';
|
|
5
|
+
import { systemNotificationTimers } from '../notifications/digest.js';
|
|
6
|
+
import { outlived } from './policy.js';
|
|
7
|
+
/**
|
|
8
|
+
* The retention sweep (TASK-135): what removes the personal data a site has
|
|
9
|
+
* kept for longer than it said it would.
|
|
10
|
+
*
|
|
11
|
+
* The files are the truth (decision-9), so the sweep reads them rather than
|
|
12
|
+
* the index: every comment file under `content/_data/comments/` and every
|
|
13
|
+
* message under `data/contact/`, each rewritten inside the lock every other
|
|
14
|
+
* writer of that file takes, and each comment it changes put back into the
|
|
15
|
+
* index in the same step. Removing a value that is already gone is not a
|
|
16
|
+
* change, so a sweep run twice, or run again after a crash halfway through,
|
|
17
|
+
* finishes in the same place and writes nothing the second time.
|
|
18
|
+
*/
|
|
19
|
+
/** How often the sweep runs once the site serves. */
|
|
20
|
+
export const RETENTION_SWEEP_MS = 6 * 60 * 60 * 1000;
|
|
21
|
+
/**
|
|
22
|
+
* One comment as the policy leaves it, or `undefined` when it has nothing to
|
|
23
|
+
* give up. The email goes with the reply subscription it served, since there
|
|
24
|
+
* is nowhere left to send one; the thread, the status and the words stay.
|
|
25
|
+
*/
|
|
26
|
+
export function expireComment(comment, policy, now) {
|
|
27
|
+
const email = comment.author.email !== null && outlived(comment.submitted, policy.commentEmailDays, now);
|
|
28
|
+
const address = comment.addressHash !== null && outlived(comment.submitted, policy.addressHashDays, now);
|
|
29
|
+
if (!email && !address)
|
|
30
|
+
return undefined;
|
|
31
|
+
return {
|
|
32
|
+
...comment,
|
|
33
|
+
author: email ? { ...comment.author, email: null } : comment.author,
|
|
34
|
+
addressHash: address ? null : comment.addressHash,
|
|
35
|
+
notify: email ? false : comment.notify,
|
|
36
|
+
redacted: withRedacted(comment.redacted, [
|
|
37
|
+
...(email ? ['email'] : []),
|
|
38
|
+
...(address ? ['addressHash'] : []),
|
|
39
|
+
]),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* One contact message under the policy: kept as it is, deleted outright, or
|
|
44
|
+
* kept without the hash of the address it came from. A message is its
|
|
45
|
+
* sender's words and address together, so it has no smaller form to shrink
|
|
46
|
+
* to once its own period is up.
|
|
47
|
+
*/
|
|
48
|
+
export function expireContactMessage(message, policy, now) {
|
|
49
|
+
if (outlived(message.received, policy.contactMessageDays, now))
|
|
50
|
+
return { kind: 'delete' };
|
|
51
|
+
if (message.addressHash !== null && outlived(message.received, policy.addressHashDays, now)) {
|
|
52
|
+
return { kind: 'redact', message: { ...message, addressHash: null } };
|
|
53
|
+
}
|
|
54
|
+
return { kind: 'keep' };
|
|
55
|
+
}
|
|
56
|
+
/** `held` with `added` joined in, in {@link REDACTED_FIELDS} order. */
|
|
57
|
+
export function withRedacted(held, added) {
|
|
58
|
+
const all = new Set([...(held ?? []), ...added]);
|
|
59
|
+
return REDACTED_FIELDS.filter((field) => all.has(field));
|
|
60
|
+
}
|
|
61
|
+
const NOTHING = { comments: 0, messagesDeleted: 0, messagesRedacted: 0 };
|
|
62
|
+
/** Build the retention sweep for one site. */
|
|
63
|
+
export function createRetentionService(options) {
|
|
64
|
+
const { admin, config } = options;
|
|
65
|
+
const timers = options.timers ?? systemNotificationTimers;
|
|
66
|
+
const logger = options.logger ?? console;
|
|
67
|
+
let sweeping = Promise.resolve();
|
|
68
|
+
let handle;
|
|
69
|
+
async function sweepOnce() {
|
|
70
|
+
// Read every time, so a period changed on the Discussion screen holds from
|
|
71
|
+
// the next sweep without a restart.
|
|
72
|
+
const policy = retentionPolicyOf(readSiteSettings(config.contentDir));
|
|
73
|
+
const now = config.now();
|
|
74
|
+
const records = { admin, contentDir: config.contentDir };
|
|
75
|
+
let comments = 0;
|
|
76
|
+
for (const slug of commentSlugs(config.contentDir)) {
|
|
77
|
+
const changed = await rewriteComments(records, slug, (comment) => expireComment(comment, policy, now));
|
|
78
|
+
comments += changed.length;
|
|
79
|
+
}
|
|
80
|
+
let messagesDeleted = 0;
|
|
81
|
+
let messagesRedacted = 0;
|
|
82
|
+
for (const listed of listContactMessages(config.dataDir)) {
|
|
83
|
+
const expiry = expireContactMessage(listed, policy, now);
|
|
84
|
+
if (expiry.kind === 'delete') {
|
|
85
|
+
if (await deleteContactMessage(config.dataDir, listed.id))
|
|
86
|
+
messagesDeleted += 1;
|
|
87
|
+
}
|
|
88
|
+
else if (expiry.kind === 'redact') {
|
|
89
|
+
let redacted = false;
|
|
90
|
+
// Decided again on the message as the lock finds it, so a Mark read
|
|
91
|
+
// that landed since the listing is kept.
|
|
92
|
+
await updateContactMessage(config.dataDir, listed.id, (current) => {
|
|
93
|
+
const again = expireContactMessage(current, policy, now);
|
|
94
|
+
if (again.kind !== 'redact')
|
|
95
|
+
return undefined;
|
|
96
|
+
redacted = true;
|
|
97
|
+
return again.message;
|
|
98
|
+
});
|
|
99
|
+
if (redacted)
|
|
100
|
+
messagesRedacted += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { comments, messagesDeleted, messagesRedacted };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
sweep() {
|
|
107
|
+
const next = sweeping.then(sweepOnce).catch((thrown) => {
|
|
108
|
+
logger.warn(`The retention sweep failed: ${messageOf(thrown)}`);
|
|
109
|
+
return NOTHING;
|
|
110
|
+
});
|
|
111
|
+
sweeping = next;
|
|
112
|
+
return next;
|
|
113
|
+
},
|
|
114
|
+
start() {
|
|
115
|
+
if (handle !== undefined)
|
|
116
|
+
return;
|
|
117
|
+
void this.sweep();
|
|
118
|
+
handle = timers.set(() => void this.sweep(), RETENTION_SWEEP_MS);
|
|
119
|
+
},
|
|
120
|
+
stop() {
|
|
121
|
+
if (handle !== undefined)
|
|
122
|
+
timers.clear(handle);
|
|
123
|
+
handle = undefined;
|
|
124
|
+
},
|
|
125
|
+
async settled() {
|
|
126
|
+
await sweeping;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function messageOf(error) {
|
|
131
|
+
return error instanceof Error ? error.message : String(error);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/privacy/retention.ts"],"names":[],"mappings":"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"}
|
|
@@ -62,7 +62,10 @@ export interface InteractionAuthor {
|
|
|
62
62
|
readonly handle: string | null;
|
|
63
63
|
/** Their profile page, for a reader following the link. */
|
|
64
64
|
readonly url: string | null;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Their avatar, when the site knows one: always a same-origin path, never
|
|
67
|
+
* the remote URL, so a reader's browser asks this site for it (TASK-134).
|
|
68
|
+
*/
|
|
66
69
|
readonly avatar: string | null;
|
|
67
70
|
/** Their id, which is what identifies them however they are named. */
|
|
68
71
|
readonly actorId: string | null;
|
|
@@ -130,6 +133,11 @@ export interface ConversationContext {
|
|
|
130
133
|
* showing while the post it is about is there to read.
|
|
131
134
|
*/
|
|
132
135
|
readonly store: ContentStore;
|
|
136
|
+
/**
|
|
137
|
+
* Where the quote approvals are: a quote of a post is shown only while the
|
|
138
|
+
* post's author vouches for it (FEP-044f, TASK-171).
|
|
139
|
+
*/
|
|
140
|
+
readonly contentDir: string;
|
|
133
141
|
/** The site's public origin, for the post's object id. */
|
|
134
142
|
readonly baseUrl: string;
|
|
135
143
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/web/conversation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/web/conversation.ts"],"names":[],"mappings":"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"}
|
package/dist/web/conversation.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { avatarHref } from '../avatars/avatars.js';
|
|
1
2
|
import { postLabel } from '../content/post-type.js';
|
|
2
|
-
import {
|
|
3
|
+
import { readAllQuoteAuthorizations } from '../federation/quotes.js';
|
|
4
|
+
import { actorHandle, quoteFrom, replyFrom, REPLY_ACTIVITY_TYPE } from '../federation/replies.js';
|
|
3
5
|
import { activityStreamsId, isPublicDocument, permalinkOfObjectId } from './documents.js';
|
|
4
6
|
import { absoluteUrl } from './negotiate.js';
|
|
5
7
|
import { sanitizeCommentHtml } from './sanitize.js';
|
|
@@ -42,14 +44,17 @@ function postConversation(context, document) {
|
|
|
42
44
|
const root = objectId ?? document.permalink;
|
|
43
45
|
const activities = objectId === undefined ? [] : activitiesAround(context.admin, objectId);
|
|
44
46
|
const native = approvedComments(context.admin, document);
|
|
45
|
-
if (activities.length === 0 && native.length === 0)
|
|
46
|
-
return NOTHING;
|
|
47
47
|
const naming = authorNaming(context.admin);
|
|
48
|
+
const quotes = objectId === undefined
|
|
49
|
+
? []
|
|
50
|
+
: quotesOf(context.admin, readAllQuoteAuthorizations(context.contentDir), objectId, naming);
|
|
51
|
+
if (activities.length === 0 && native.length === 0 && quotes.length === 0)
|
|
52
|
+
return NOTHING;
|
|
48
53
|
const withdrawn = withdrawnBy(activities);
|
|
49
54
|
const written = [];
|
|
50
55
|
const likes = [];
|
|
51
56
|
const boosts = [];
|
|
52
|
-
const mentions = [];
|
|
57
|
+
const mentions = [...quotes];
|
|
53
58
|
const reacted = new Set();
|
|
54
59
|
for (const activity of activities) {
|
|
55
60
|
const kind = REACTIONS[activity.activityType];
|
|
@@ -154,6 +159,16 @@ function siteConversation(context, limit) {
|
|
|
154
159
|
break;
|
|
155
160
|
}
|
|
156
161
|
}
|
|
162
|
+
// Every quote the site vouches for, rather than a page of them: there is one
|
|
163
|
+
// approval per quote, and a site has few enough to read whole.
|
|
164
|
+
for (const approval of readAllQuoteAuthorizations(context.contentDir)) {
|
|
165
|
+
const post = posts.get(approval.post);
|
|
166
|
+
if (post === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
for (const quote of quotesOf(context.admin, [approval], approval.post, naming)) {
|
|
169
|
+
said.push({ ...quote, post });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
157
172
|
for (const stored of context.admin.listComments({
|
|
158
173
|
status: 'approved',
|
|
159
174
|
limit: limit * OVERSCAN,
|
|
@@ -168,9 +183,14 @@ function siteConversation(context, limit) {
|
|
|
168
183
|
/** How many answers each of these posts has, by permalink. */
|
|
169
184
|
function commentCounts(context, documents) {
|
|
170
185
|
const counts = new Map();
|
|
186
|
+
const approvals = readAllQuoteAuthorizations(context.contentDir);
|
|
187
|
+
const naming = authorNaming(context.admin);
|
|
171
188
|
for (const document of documents) {
|
|
172
189
|
const objectId = activityStreamsId(document, context.baseUrl);
|
|
173
|
-
const federated = objectId === undefined
|
|
190
|
+
const federated = objectId === undefined
|
|
191
|
+
? 0
|
|
192
|
+
: context.admin.countRepliesTo(objectId) +
|
|
193
|
+
quotesOf(context.admin, approvals, objectId, naming).length;
|
|
174
194
|
counts.set(document.permalink, federated + context.admin.countCommentsFor(document.slug, 'approved'));
|
|
175
195
|
}
|
|
176
196
|
return counts;
|
|
@@ -184,6 +204,46 @@ function commentCounts(context, documents) {
|
|
|
184
204
|
* whose posts do not.
|
|
185
205
|
*/
|
|
186
206
|
const OVERSCAN = 4;
|
|
207
|
+
/**
|
|
208
|
+
* The approved quotes of one post, as mentions, in no order.
|
|
209
|
+
*
|
|
210
|
+
* The approval is what makes a quote showable (FEP-044f: a quote its author
|
|
211
|
+
* never approved should not be displayed), so the approvals are walked and the
|
|
212
|
+
* log is asked for each one's note, rather than the other way about. The note
|
|
213
|
+
* has to be the approved one, by the approved actor, quoting this post: an
|
|
214
|
+
* approval of one quote vouches for nothing else. Both are files (decision-9),
|
|
215
|
+
* so a rebuilt index shows exactly what the live one did.
|
|
216
|
+
*/
|
|
217
|
+
function quotesOf(admin, approvals, objectId, naming) {
|
|
218
|
+
const quotes = [];
|
|
219
|
+
for (const approval of approvals) {
|
|
220
|
+
if (approval.post !== objectId)
|
|
221
|
+
continue;
|
|
222
|
+
for (const activity of admin.listActivitiesAbout([approval.quote])) {
|
|
223
|
+
if (activity.actorId !== approval.actor)
|
|
224
|
+
continue;
|
|
225
|
+
const quote = quoteFrom(activity);
|
|
226
|
+
if (quote?.id !== approval.quote || quote.quoted !== objectId)
|
|
227
|
+
continue;
|
|
228
|
+
quotes.push({
|
|
229
|
+
id: quote.id,
|
|
230
|
+
source: 'activitypub',
|
|
231
|
+
kind: 'mention',
|
|
232
|
+
author: naming(quote.actorId),
|
|
233
|
+
url: quote.url,
|
|
234
|
+
content: sanitizeCommentHtml(quote.html),
|
|
235
|
+
published: quote.published,
|
|
236
|
+
inReplyTo: null,
|
|
237
|
+
status: 'published',
|
|
238
|
+
replies: [],
|
|
239
|
+
});
|
|
240
|
+
// The first `Create` of the note is the one shown: a redelivery is the
|
|
241
|
+
// same quote again.
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return quotes;
|
|
246
|
+
}
|
|
187
247
|
/** One logged fediverse reply as the thread's own shape. */
|
|
188
248
|
function federatedInteraction(reply, author) {
|
|
189
249
|
return {
|
|
@@ -229,7 +289,7 @@ function interactionOf(comment, permalink) {
|
|
|
229
289
|
url: comment.author.url,
|
|
230
290
|
// Only a webmention has one: it came out of the source page's `h-card`,
|
|
231
291
|
// and a form asks nobody for a picture.
|
|
232
|
-
avatar: comment.author.avatar,
|
|
292
|
+
avatar: comment.author.avatar === null ? null : avatarHref(comment.author.avatar),
|
|
233
293
|
actorId: null,
|
|
234
294
|
},
|
|
235
295
|
// Where it can be read. A comment written here lives here, at its own
|
|
@@ -513,11 +573,12 @@ function authorNaming(admin) {
|
|
|
513
573
|
profiles ??= new Map(admin.listFollowers().map((entry) => [entry.actorId, entry]));
|
|
514
574
|
const follower = profiles.get(actorId);
|
|
515
575
|
const handle = follower?.handle ?? actorHandle(actorId) ?? null;
|
|
576
|
+
const icon = follower?.iconUrl ?? null;
|
|
516
577
|
const author = {
|
|
517
578
|
name: follower?.name ?? handle ?? actorId,
|
|
518
579
|
handle,
|
|
519
580
|
url: follower?.url ?? actorId,
|
|
520
|
-
avatar:
|
|
581
|
+
avatar: icon === null ? null : avatarHref(icon),
|
|
521
582
|
actorId,
|
|
522
583
|
};
|
|
523
584
|
known.set(actorId, author);
|