@anthusai/papyrus 2.0.0-next.41 → 2.0.0-next.43
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/amplify/functions/assignment-action/data-access.js +2 -0
- package/amplify/functions/assignment-action/data-access.ts +2 -0
- package/amplify/functions/content-actions/handler.py +1 -1
- package/amplify/functions/shared/editorial-context.ts +59 -6
- package/amplify/plugins/newsletter/config.js +195 -0
- package/amplify/plugins/newsletter/config.ts +240 -0
- package/amplify/plugins/newsletter/function/handler.py +6 -0
- package/amplify/plugins/newsletter/function/requirements.txt +5 -0
- package/amplify/plugins/newsletter/ops/handler.py +12 -0
- package/amplify/plugins/newsletter/ops/resource.js +27 -0
- package/amplify/plugins/newsletter/ops/resource.ts +37 -0
- package/amplify/plugins/newsletter/papyrus.plugin.js +246 -0
- package/amplify/plugins/newsletter/papyrus.plugin.ts +291 -0
- package/amplify/plugins/newsletter/schema.js +37 -0
- package/amplify/plugins/newsletter/schema.ts +42 -0
- package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
- package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
- package/amplify/plugins/newsletter/sending.js +139 -0
- package/amplify/plugins/newsletter/sending.ts +185 -0
- package/amplify/plugins/reader-steering/config.js +55 -3
- package/amplify/plugins/reader-steering/config.ts +98 -3
- package/amplify/plugins/reader-steering/function/ai_moderation.py +230 -0
- package/amplify/plugins/reader-steering/function/handler.py +829 -10
- package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
- package/amplify/plugins/reader-steering/function/moderation.py +316 -0
- package/amplify/plugins/reader-steering/function/resource.js +1 -1
- package/amplify/plugins/reader-steering/function/resource.ts +1 -1
- package/amplify/plugins/reader-steering/papyrus.plugin.js +78 -6
- package/amplify/plugins/reader-steering/papyrus.plugin.ts +85 -6
- package/amplify/plugins/reader-steering/schema.js +101 -1
- package/amplify/plugins/reader-steering/schema.ts +101 -1
- package/amplify/plugins/registry.js +2 -0
- package/amplify/plugins/registry.ts +2 -0
- package/amplify/plugins/types.ts +2 -0
- package/app/api/newsletter/confirm/route.ts +13 -0
- package/app/api/newsletter/preferences/route.ts +13 -0
- package/app/api/newsletter/subscribe/route.ts +9 -0
- package/app/api/newsletter/unsubscribe/route.ts +13 -0
- package/app/api/reader/comment/delete/route.ts +9 -0
- package/app/api/reader/comment/edit/route.ts +9 -0
- package/app/api/reader/comment/route.ts +9 -0
- package/app/api/reader/report/route.ts +9 -0
- package/app/api/reader/submit/route.ts +9 -0
- package/components/moderation-panel.tsx +248 -0
- package/components/topic-steering-workspace.tsx +19 -1
- package/lib/editorial-checks.ts +3 -1
- package/lib/newsletter-api.ts +391 -0
- package/lib/newsletter-pages.ts +264 -0
- package/lib/reader-api.ts +7 -5
- package/lib/reader-moderation.ts +220 -0
- package/package.json +14 -1
- package/routes.manifest.json +103 -1
|
@@ -15,14 +15,16 @@ export type { ReaderSteeringConfig };
|
|
|
15
15
|
export const READER_STEERING_OUTPUT_KEY = "readerSteering";
|
|
16
16
|
export const SNAPSHOT_SCHEDULE_PREFIX = "papyrus-reader-snapshot-";
|
|
17
17
|
export const READER_SNAPSHOT_TASK = { projectionTask: { task: "reader-snapshot" } };
|
|
18
|
+
export const READER_SCHEDULED_SNAPSHOT_TASK = { projectionTask: { task: "reader-snapshot", retirementPoll: true } };
|
|
19
|
+
export const READER_POLL_SECRET_PARAMETER_ENV = "PAPYRUS_READER_POLL_SECRET_PARAMETER";
|
|
18
20
|
|
|
19
21
|
export const readerSteeringManifest: PluginManifest = {
|
|
20
22
|
name: "reader-steering",
|
|
21
23
|
contract: 1,
|
|
22
|
-
models: ["ReaderProfile", "ReaderHandle", "ReaderVote"],
|
|
23
|
-
mutations: [],
|
|
24
|
+
models: ["ReaderProfile", "ReaderHandle", "ReaderVote", "ReaderSubmission", "ReaderComment", "ReaderReport", "ModerationAction"],
|
|
25
|
+
mutations: ["moderateReader"],
|
|
24
26
|
guest: { mutations: [] },
|
|
25
|
-
newsroomPanels: [],
|
|
27
|
+
newsroomPanels: [{ id: "moderation", label: "Moderation" }],
|
|
26
28
|
staticAssets: ["_plugins/papyrus-reader@1.js", "_plugins/papyrus-reader@1.css"],
|
|
27
29
|
};
|
|
28
30
|
|
|
@@ -31,6 +33,9 @@ export function readerActionsSettings(config: ReaderSteeringConfig, allowInterim
|
|
|
31
33
|
return {
|
|
32
34
|
dimensions: config.dimensions,
|
|
33
35
|
limits: config.limits,
|
|
36
|
+
submissions: config.submissions,
|
|
37
|
+
comments: config.comments,
|
|
38
|
+
moderation: config.moderation,
|
|
34
39
|
reservedHandles: config.reservedHandles,
|
|
35
40
|
allowInterimSubjects,
|
|
36
41
|
snapshotDebounceSeconds: 60,
|
|
@@ -41,6 +46,7 @@ export function readerActionsSettings(config: ReaderSteeringConfig, allowInterim
|
|
|
41
46
|
export function readerSnapshotSettings(config: ReaderSteeringConfig) {
|
|
42
47
|
return {
|
|
43
48
|
dimensions: config.dimensions,
|
|
49
|
+
submissions: config.submissions,
|
|
44
50
|
karma: config.karma,
|
|
45
51
|
ranking: config.ranking,
|
|
46
52
|
hypeMinimum: config.snapshot.hypeMinimum,
|
|
@@ -53,12 +59,66 @@ export function interimSubjectsAllowedAtSynth(environment: Record<string, string
|
|
|
53
59
|
return ["development", "test"].includes((environment.SITE_ENV ?? "").trim()) && environment.NODE_ENV !== "production";
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The scheduled run polls Prospectus through the CMS route /api/reader/retired (docs/reader-steering.md), so it needs the
|
|
64
|
+
* site origin and the poll secret parameter that the app-shell stack generates (`cms.readerRetirementPoll`); both reach the
|
|
65
|
+
* backend build as branch variables. It also deletes and re-keys rows, and keeps its cursor in the rate table.
|
|
66
|
+
*/
|
|
67
|
+
function wireRetirementPoll(readerLambda: LambdaFunction, contentActions: LambdaFunction, tables: Record<string, ITable>) {
|
|
68
|
+
const origin = (process.env.PAPYRUS_SITE_ORIGIN ?? "").trim();
|
|
69
|
+
const parameterName = (process.env[READER_POLL_SECRET_PARAMETER_ENV] ?? "").trim();
|
|
70
|
+
if (!origin.startsWith("https://") || !parameterName.startsWith("/")) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"reader-steering retirementPoll needs PAPYRUS_SITE_ORIGIN (an https origin) and PAPYRUS_READER_POLL_SECRET_PARAMETER (an SSM parameter name). The app-shell template sets both on the production branch when site.json has cms.readerRetirementPoll: true; see docs/reader-steering.md.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const rateTable = readerLambda.node.scope?.node.tryFindChild("PapyrusReaderRateWindows") as ITable | undefined;
|
|
76
|
+
if (!rateTable) throw new Error("reader-steering retirementPoll could not find the reader-actions rate table to keep its cursor in");
|
|
77
|
+
tables.ReaderVote.grantReadWriteData(contentActions);
|
|
78
|
+
tables.ReaderHandle.grantReadWriteData(contentActions);
|
|
79
|
+
rateTable.grantReadWriteData(contentActions);
|
|
80
|
+
contentActions.addEnvironment("PAPYRUS_READER_RATE_TABLE", rateTable.tableName);
|
|
81
|
+
contentActions.addEnvironment("PAPYRUS_READER_POLL_URL", origin);
|
|
82
|
+
contentActions.addEnvironment(READER_POLL_SECRET_PARAMETER_ENV, parameterName);
|
|
83
|
+
const stack = Stack.of(contentActions);
|
|
84
|
+
contentActions.addToRolePolicy(
|
|
85
|
+
new PolicyStatement({
|
|
86
|
+
effect: Effect.ALLOW,
|
|
87
|
+
actions: ["ssm:GetParameter"],
|
|
88
|
+
resources: [`arn:${stack.partition}:ssm:${stack.region}:${stack.account}:parameter${parameterName}`],
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* AI moderation (`moderation.mode: ai`). The debounced run becomes a run of reader-actions itself (a one-off schedule that
|
|
95
|
+
* invokes this function), which judges the new items and then asks content-actions for the snapshot. So the scheduler role may
|
|
96
|
+
* invoke reader-actions, reader-actions may invoke content-actions (its ARN is already in the environment) and may read exactly
|
|
97
|
+
* one SSM SecureString: the API key, which the plugin never creates and never receives.
|
|
98
|
+
*/
|
|
99
|
+
function wireAiModeration(readerLambda: LambdaFunction, contentActions: LambdaFunction, schedulerRole: Role, apiKeyParameter: string) {
|
|
100
|
+
const stack = Stack.of(readerLambda);
|
|
101
|
+
readerLambda.grantInvoke(schedulerRole);
|
|
102
|
+
contentActions.grantInvoke(readerLambda);
|
|
103
|
+
readerLambda.addToRolePolicy(
|
|
104
|
+
new PolicyStatement({
|
|
105
|
+
effect: Effect.ALLOW,
|
|
106
|
+
actions: ["ssm:GetParameter"],
|
|
107
|
+
resources: [`arn:${stack.partition}:ssm:${stack.region}:${stack.account}:parameter${apiKeyParameter}`],
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
56
112
|
export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerActions: typeof readerActions }> = {
|
|
57
113
|
manifest: readerSteeringManifest,
|
|
58
114
|
resolveConfig: resolveReaderSteeringConfig,
|
|
59
115
|
functions: () => ({ readerActions }),
|
|
60
116
|
schemaDefinitions: readerSteeringSchemaDefinitions,
|
|
61
|
-
publicSettings: (config) => ({
|
|
117
|
+
publicSettings: (config) => ({
|
|
118
|
+
contract: readerSteeringManifest.contract,
|
|
119
|
+
dimensions: config.dimensions,
|
|
120
|
+
moderation: { mode: config.moderation.mode },
|
|
121
|
+
}),
|
|
62
122
|
wire(backend, config, context: PluginWiringContext) {
|
|
63
123
|
const lambda = backend.readerActions.resources.lambda as LambdaFunction;
|
|
64
124
|
const tables = backend.data.resources.tables as Record<string, ITable>;
|
|
@@ -68,15 +128,25 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
68
128
|
["PAPYRUS_READER_PROFILE_TABLE", "ReaderProfile"],
|
|
69
129
|
["PAPYRUS_READER_HANDLE_TABLE", "ReaderHandle"],
|
|
70
130
|
["PAPYRUS_READER_VOTE_TABLE", "ReaderVote"],
|
|
131
|
+
["PAPYRUS_READER_SUBMISSION_TABLE", "ReaderSubmission"],
|
|
132
|
+
["PAPYRUS_READER_COMMENT_TABLE", "ReaderComment"],
|
|
133
|
+
["PAPYRUS_READER_REPORT_TABLE", "ReaderReport"],
|
|
134
|
+
["PAPYRUS_READER_ACTION_TABLE", "ModerationAction"],
|
|
71
135
|
] as const) {
|
|
72
136
|
tables[model].grantReadWriteData(lambda);
|
|
73
137
|
lambda.addEnvironment(variable, tables[model].tableName);
|
|
74
138
|
contentActions.addEnvironment(variable, tables[model].tableName);
|
|
75
139
|
}
|
|
140
|
+
// Promoting a submission writes one draft Item (a version 1 draft; nothing is published by this function).
|
|
141
|
+
tables.Item.grantWriteData(lambda);
|
|
142
|
+
lambda.addEnvironment("PAPYRUS_READER_ITEM_TABLE", tables.Item.tableName);
|
|
76
143
|
tables.PublishedItem.grantReadData(lambda);
|
|
77
144
|
lambda.addEnvironment("PAPYRUS_READER_PUBLISHED_ITEM_TABLE", tables.PublishedItem.tableName);
|
|
78
145
|
tables.ReaderVote.grantReadData(contentActions);
|
|
79
146
|
tables.ReaderProfile.grantReadWriteData(contentActions);
|
|
147
|
+
// The snapshot reads submissions, and the review step writes their signals and moderation state.
|
|
148
|
+
tables.ReaderSubmission.grantReadWriteData(contentActions);
|
|
149
|
+
tables.ReaderComment.grantReadWriteData(contentActions);
|
|
80
150
|
|
|
81
151
|
lambda.addEnvironment("PAPYRUS_READER_SETTINGS", JSON.stringify(readerActionsSettings(config, interimSubjectsAllowedAtSynth())));
|
|
82
152
|
if (config.reservedConcurrency !== undefined) {
|
|
@@ -104,16 +174,25 @@ export const readerSteeringPlugin: PapyrusPlugin<ReaderSteeringConfig, { readerA
|
|
|
104
174
|
lambda.addEnvironment("PAPYRUS_READER_SCHEDULER_ROLE_ARN", schedulerRole.roleArn);
|
|
105
175
|
contentActions.addEnvironment("PAPYRUS_READER_SNAPSHOT_SETTINGS", JSON.stringify(readerSnapshotSettings(config)));
|
|
106
176
|
|
|
177
|
+
if (config.moderation.ai) wireAiModeration(lambda, contentActions, schedulerRole, config.moderation.ai.apiKeyParameter);
|
|
178
|
+
|
|
107
179
|
lambda.addPermission("InvokeFromCmsCompute", {
|
|
108
180
|
principal: new ArnPrincipal(config.computeRoleArn),
|
|
109
181
|
action: "lambda:InvokeFunction",
|
|
110
182
|
});
|
|
111
183
|
|
|
184
|
+
if (config.retirementPoll) wireRetirementPoll(lambda, contentActions, tables);
|
|
185
|
+
|
|
112
186
|
if (config.snapshot.schedule) {
|
|
113
187
|
new Rule(context.createStack("reader-steering-schedule"), "ReaderSnapshotSchedule", {
|
|
114
|
-
description: "Rebuilds the reader snapshot (time decay, Rising) and
|
|
188
|
+
description: "Rebuilds the reader snapshot (time decay, Rising) and, when enabled, applies Prospectus subject retirements.",
|
|
115
189
|
schedule: Schedule.rate(Duration.hours(config.snapshot.intervalHours)),
|
|
116
|
-
targets: [
|
|
190
|
+
targets: [
|
|
191
|
+
new LambdaFunctionTarget(contentActions, {
|
|
192
|
+
event: RuleTargetInput.fromObject(config.retirementPoll ? READER_SCHEDULED_SNAPSHOT_TASK : READER_SNAPSHOT_TASK),
|
|
193
|
+
retryAttempts: 0,
|
|
194
|
+
}),
|
|
195
|
+
],
|
|
117
196
|
});
|
|
118
197
|
}
|
|
119
198
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { readerActions } from "./function/resource.js";
|
|
2
3
|
/**
|
|
3
4
|
* Staff and IAM only: no guest, API key or reader rule on any model. The reader-actions function writes the
|
|
4
5
|
* tables directly with its own role, and editors may list them; admins may delete. `id` is the lookup key:
|
|
5
|
-
* the siteSubject for a profile, the lowercase handle for a claim
|
|
6
|
+
* the siteSubject for a profile, the lowercase handle for a claim, `subjectKey#readerId#dimension` for a vote and
|
|
7
|
+
* `sub-<sha256 of the canonical URL>` for a submission (so a canonical URL is one submission).
|
|
6
8
|
*/
|
|
7
9
|
const staffOnly = (allow) => [allow.group("editor").to(["read"]), allow.group("admin").to(["read", "delete"])];
|
|
8
10
|
export function readerSteeringSchemaDefinitions() {
|
|
@@ -17,6 +19,7 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
17
19
|
karma: a.integer(),
|
|
18
20
|
})
|
|
19
21
|
.disableOperations(["subscriptions"])
|
|
22
|
+
.secondaryIndexes((index) => [index("readerId").name("profilesByReader").queryField("listReaderProfilesByReader")])
|
|
20
23
|
.authorization(staffOnly),
|
|
21
24
|
ReaderHandle: a
|
|
22
25
|
.model({
|
|
@@ -36,5 +39,102 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
36
39
|
})
|
|
37
40
|
.disableOperations(["subscriptions"])
|
|
38
41
|
.authorization(staffOnly),
|
|
42
|
+
ReaderComment: a
|
|
43
|
+
.model({
|
|
44
|
+
id: a.id().required(),
|
|
45
|
+
subjectKey: a.string().required(),
|
|
46
|
+
readerId: a.string().required(),
|
|
47
|
+
body: a.string().required(),
|
|
48
|
+
parentId: a.string(),
|
|
49
|
+
ratingContext: a.json(),
|
|
50
|
+
state: a.string().required(),
|
|
51
|
+
signals: a.json(),
|
|
52
|
+
moderation: a.string().required(),
|
|
53
|
+
reportCount: a.integer(),
|
|
54
|
+
reviewedAt: a.datetime(),
|
|
55
|
+
editedAt: a.datetime(),
|
|
56
|
+
deletedAt: a.datetime(),
|
|
57
|
+
createdAt: a.datetime().required(),
|
|
58
|
+
})
|
|
59
|
+
.disableOperations(["subscriptions"])
|
|
60
|
+
.secondaryIndexes((index) => [index("moderation").sortKeys(["createdAt"]).name("commentsByModeration").queryField("listReaderCommentsByModerationAndCreatedAt")])
|
|
61
|
+
.authorization(staffOnly),
|
|
62
|
+
ReaderSubmission: a
|
|
63
|
+
.model({
|
|
64
|
+
id: a.id().required(),
|
|
65
|
+
canonicalUrl: a.string().required(),
|
|
66
|
+
domain: a.string().required(),
|
|
67
|
+
title: a.string().required(),
|
|
68
|
+
kind: a.string().required(),
|
|
69
|
+
note: a.string(),
|
|
70
|
+
readerId: a.string().required(),
|
|
71
|
+
state: a.string().required(),
|
|
72
|
+
similar: a.string().array(),
|
|
73
|
+
signals: a.json(),
|
|
74
|
+
extractionState: a.string().required(),
|
|
75
|
+
itemId: a.string(),
|
|
76
|
+
moderation: a.string().required(),
|
|
77
|
+
reportCount: a.integer(),
|
|
78
|
+
reviewedAt: a.datetime(),
|
|
79
|
+
createdAt: a.datetime().required(),
|
|
80
|
+
})
|
|
81
|
+
.disableOperations(["subscriptions"])
|
|
82
|
+
.secondaryIndexes((index) => [
|
|
83
|
+
index("domain").sortKeys(["createdAt"]).name("submissionsByDomain").queryField("listReaderSubmissionsByDomainAndCreatedAt"),
|
|
84
|
+
index("moderation").sortKeys(["createdAt"]).name("submissionsByModeration").queryField("listReaderSubmissionsByModerationAndCreatedAt"),
|
|
85
|
+
])
|
|
86
|
+
.authorization(staffOnly),
|
|
87
|
+
ReaderReport: a
|
|
88
|
+
.model({
|
|
89
|
+
id: a.id().required(),
|
|
90
|
+
targetType: a.string().required(),
|
|
91
|
+
target: a.string().required(),
|
|
92
|
+
reporterReaderId: a.string().required(),
|
|
93
|
+
reason: a.string().required(),
|
|
94
|
+
state: a.string().required(),
|
|
95
|
+
closedAt: a.datetime(),
|
|
96
|
+
createdAt: a.datetime().required(),
|
|
97
|
+
})
|
|
98
|
+
.disableOperations(["subscriptions"])
|
|
99
|
+
.secondaryIndexes((index) => [index("target").sortKeys(["createdAt"]).name("reportsByTarget").queryField("listReaderReportsByTargetAndCreatedAt")])
|
|
100
|
+
.authorization(staffOnly),
|
|
101
|
+
ModerationAction: a
|
|
102
|
+
.model({
|
|
103
|
+
id: a.id().required(),
|
|
104
|
+
by: a.string().required(),
|
|
105
|
+
actorSub: a.string().required(),
|
|
106
|
+
action: a.string().required(),
|
|
107
|
+
targetType: a.string().required(),
|
|
108
|
+
target: a.string().required(),
|
|
109
|
+
reason: a.string(),
|
|
110
|
+
fromState: a.string().required(),
|
|
111
|
+
toState: a.string().required(),
|
|
112
|
+
fromModeration: a.string(),
|
|
113
|
+
toModeration: a.string(),
|
|
114
|
+
undoOf: a.string(),
|
|
115
|
+
undoneBy: a.string(),
|
|
116
|
+
at: a.datetime().required(),
|
|
117
|
+
})
|
|
118
|
+
.disableOperations(["subscriptions"])
|
|
119
|
+
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
120
|
+
.authorization(staffOnly),
|
|
121
|
+
ModerationReceipt: a.customType({
|
|
122
|
+
status: a.string().required(),
|
|
123
|
+
id: a.string(),
|
|
124
|
+
error: a.string(),
|
|
125
|
+
message: a.string(),
|
|
126
|
+
}),
|
|
127
|
+
moderateReader: a
|
|
128
|
+
.mutation()
|
|
129
|
+
.arguments({
|
|
130
|
+
action: a.string().required(),
|
|
131
|
+
targetType: a.string().required(),
|
|
132
|
+
target: a.string().required(),
|
|
133
|
+
reason: a.string(),
|
|
134
|
+
undoOf: a.string(),
|
|
135
|
+
})
|
|
136
|
+
.returns(a.ref("ModerationReceipt"))
|
|
137
|
+
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
138
|
+
.handler(a.handler.function(readerActions)),
|
|
39
139
|
};
|
|
40
140
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { a } from "@aws-amplify/backend";
|
|
2
|
+
import { readerActions } from "./function/resource";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Staff and IAM only: no guest, API key or reader rule on any model. The reader-actions function writes the
|
|
5
6
|
* tables directly with its own role, and editors may list them; admins may delete. `id` is the lookup key:
|
|
6
|
-
* the siteSubject for a profile, the lowercase handle for a claim
|
|
7
|
+
* the siteSubject for a profile, the lowercase handle for a claim, `subjectKey#readerId#dimension` for a vote and
|
|
8
|
+
* `sub-<sha256 of the canonical URL>` for a submission (so a canonical URL is one submission).
|
|
7
9
|
*/
|
|
8
10
|
const staffOnly = (allow: any) => [allow.group("editor").to(["read"]), allow.group("admin").to(["read", "delete"])];
|
|
9
11
|
|
|
@@ -19,6 +21,7 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
19
21
|
karma: a.integer(),
|
|
20
22
|
})
|
|
21
23
|
.disableOperations(["subscriptions"])
|
|
24
|
+
.secondaryIndexes((index) => [index("readerId").name("profilesByReader").queryField("listReaderProfilesByReader")])
|
|
22
25
|
.authorization(staffOnly),
|
|
23
26
|
ReaderHandle: a
|
|
24
27
|
.model({
|
|
@@ -38,5 +41,102 @@ export function readerSteeringSchemaDefinitions() {
|
|
|
38
41
|
})
|
|
39
42
|
.disableOperations(["subscriptions"])
|
|
40
43
|
.authorization(staffOnly),
|
|
44
|
+
ReaderComment: a
|
|
45
|
+
.model({
|
|
46
|
+
id: a.id().required(),
|
|
47
|
+
subjectKey: a.string().required(),
|
|
48
|
+
readerId: a.string().required(),
|
|
49
|
+
body: a.string().required(),
|
|
50
|
+
parentId: a.string(),
|
|
51
|
+
ratingContext: a.json(),
|
|
52
|
+
state: a.string().required(),
|
|
53
|
+
signals: a.json(),
|
|
54
|
+
moderation: a.string().required(),
|
|
55
|
+
reportCount: a.integer(),
|
|
56
|
+
reviewedAt: a.datetime(),
|
|
57
|
+
editedAt: a.datetime(),
|
|
58
|
+
deletedAt: a.datetime(),
|
|
59
|
+
createdAt: a.datetime().required(),
|
|
60
|
+
})
|
|
61
|
+
.disableOperations(["subscriptions"])
|
|
62
|
+
.secondaryIndexes((index) => [index("moderation").sortKeys(["createdAt"]).name("commentsByModeration").queryField("listReaderCommentsByModerationAndCreatedAt")])
|
|
63
|
+
.authorization(staffOnly),
|
|
64
|
+
ReaderSubmission: a
|
|
65
|
+
.model({
|
|
66
|
+
id: a.id().required(),
|
|
67
|
+
canonicalUrl: a.string().required(),
|
|
68
|
+
domain: a.string().required(),
|
|
69
|
+
title: a.string().required(),
|
|
70
|
+
kind: a.string().required(),
|
|
71
|
+
note: a.string(),
|
|
72
|
+
readerId: a.string().required(),
|
|
73
|
+
state: a.string().required(),
|
|
74
|
+
similar: a.string().array(),
|
|
75
|
+
signals: a.json(),
|
|
76
|
+
extractionState: a.string().required(),
|
|
77
|
+
itemId: a.string(),
|
|
78
|
+
moderation: a.string().required(),
|
|
79
|
+
reportCount: a.integer(),
|
|
80
|
+
reviewedAt: a.datetime(),
|
|
81
|
+
createdAt: a.datetime().required(),
|
|
82
|
+
})
|
|
83
|
+
.disableOperations(["subscriptions"])
|
|
84
|
+
.secondaryIndexes((index) => [
|
|
85
|
+
index("domain").sortKeys(["createdAt"]).name("submissionsByDomain").queryField("listReaderSubmissionsByDomainAndCreatedAt"),
|
|
86
|
+
index("moderation").sortKeys(["createdAt"]).name("submissionsByModeration").queryField("listReaderSubmissionsByModerationAndCreatedAt"),
|
|
87
|
+
])
|
|
88
|
+
.authorization(staffOnly),
|
|
89
|
+
ReaderReport: a
|
|
90
|
+
.model({
|
|
91
|
+
id: a.id().required(),
|
|
92
|
+
targetType: a.string().required(),
|
|
93
|
+
target: a.string().required(),
|
|
94
|
+
reporterReaderId: a.string().required(),
|
|
95
|
+
reason: a.string().required(),
|
|
96
|
+
state: a.string().required(),
|
|
97
|
+
closedAt: a.datetime(),
|
|
98
|
+
createdAt: a.datetime().required(),
|
|
99
|
+
})
|
|
100
|
+
.disableOperations(["subscriptions"])
|
|
101
|
+
.secondaryIndexes((index) => [index("target").sortKeys(["createdAt"]).name("reportsByTarget").queryField("listReaderReportsByTargetAndCreatedAt")])
|
|
102
|
+
.authorization(staffOnly),
|
|
103
|
+
ModerationAction: a
|
|
104
|
+
.model({
|
|
105
|
+
id: a.id().required(),
|
|
106
|
+
by: a.string().required(),
|
|
107
|
+
actorSub: a.string().required(),
|
|
108
|
+
action: a.string().required(),
|
|
109
|
+
targetType: a.string().required(),
|
|
110
|
+
target: a.string().required(),
|
|
111
|
+
reason: a.string(),
|
|
112
|
+
fromState: a.string().required(),
|
|
113
|
+
toState: a.string().required(),
|
|
114
|
+
fromModeration: a.string(),
|
|
115
|
+
toModeration: a.string(),
|
|
116
|
+
undoOf: a.string(),
|
|
117
|
+
undoneBy: a.string(),
|
|
118
|
+
at: a.datetime().required(),
|
|
119
|
+
})
|
|
120
|
+
.disableOperations(["subscriptions"])
|
|
121
|
+
.secondaryIndexes((index) => [index("target").sortKeys(["at"]).name("actionsByTarget").queryField("listModerationActionsByTargetAndAt")])
|
|
122
|
+
.authorization(staffOnly),
|
|
123
|
+
ModerationReceipt: a.customType({
|
|
124
|
+
status: a.string().required(),
|
|
125
|
+
id: a.string(),
|
|
126
|
+
error: a.string(),
|
|
127
|
+
message: a.string(),
|
|
128
|
+
}),
|
|
129
|
+
moderateReader: a
|
|
130
|
+
.mutation()
|
|
131
|
+
.arguments({
|
|
132
|
+
action: a.string().required(),
|
|
133
|
+
targetType: a.string().required(),
|
|
134
|
+
target: a.string().required(),
|
|
135
|
+
reason: a.string(),
|
|
136
|
+
undoOf: a.string(),
|
|
137
|
+
})
|
|
138
|
+
.returns(a.ref("ModerationReceipt"))
|
|
139
|
+
.authorization((allow) => [allow.group("editor"), allow.group("admin")])
|
|
140
|
+
.handler(a.handler.function(readerActions)),
|
|
41
141
|
};
|
|
42
142
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { newsletterPlugin } from "./newsletter/papyrus.plugin.js";
|
|
1
2
|
import { readerSteeringPlugin } from "./reader-steering/papyrus.plugin.js";
|
|
2
3
|
import { signupPlugin } from "./signup/papyrus.plugin.js";
|
|
3
4
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
4
5
|
export const KNOWN_PLUGINS = {
|
|
6
|
+
newsletter: newsletterPlugin,
|
|
5
7
|
"reader-steering": readerSteeringPlugin,
|
|
6
8
|
signup: signupPlugin,
|
|
7
9
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { PapyrusPlugin, PluginRequest } from "./types";
|
|
2
|
+
import { newsletterPlugin } from "./newsletter/papyrus.plugin";
|
|
2
3
|
import { readerSteeringPlugin } from "./reader-steering/papyrus.plugin";
|
|
3
4
|
import { signupPlugin } from "./signup/papyrus.plugin";
|
|
4
5
|
|
|
5
6
|
/** Plugins are packaged with Papyrus and pinned by its version: there is no registry to fetch from. */
|
|
6
7
|
export const KNOWN_PLUGINS: Record<string, PapyrusPlugin<any, any>> = {
|
|
8
|
+
newsletter: newsletterPlugin,
|
|
7
9
|
"reader-steering": readerSteeringPlugin,
|
|
8
10
|
signup: signupPlugin,
|
|
9
11
|
};
|
package/amplify/plugins/types.ts
CHANGED
|
@@ -10,6 +10,8 @@ export type PluginManifest = {
|
|
|
10
10
|
/** Public contract version. Bump it when a mutation input, a model field, an element name or an attribute changes. No compatibility shims. */
|
|
11
11
|
contract: number;
|
|
12
12
|
models: string[];
|
|
13
|
+
/** Custom queries it adds (operations that read). */
|
|
14
|
+
queries?: string[];
|
|
13
15
|
mutations: string[];
|
|
14
16
|
/** What an anonymous visitor may call. Everything else is closed to guests. */
|
|
15
17
|
guest: { mutations: string[] };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleNewsletterConfirmPage, handleNewsletterConfirmPost } from "../../../../lib/newsletter-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleNewsletterConfirmPage(request);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function POST(request: NextRequest) {
|
|
12
|
+
return handleNewsletterConfirmPost(request);
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleNewsletterPreferences } from "../../../../lib/newsletter-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleNewsletterPreferences(request);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function POST(request: NextRequest) {
|
|
12
|
+
return handleNewsletterPreferences(request);
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleNewsletterSubscribe } from "../../../../lib/newsletter-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleNewsletterSubscribe(request);
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleNewsletterUnsubscribe } from "../../../../lib/newsletter-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function GET(request: NextRequest) {
|
|
8
|
+
return handleNewsletterUnsubscribe(request);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function POST(request: NextRequest) {
|
|
12
|
+
return handleNewsletterUnsubscribe(request);
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "comment-delete");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "comment-edit");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "comment");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "report");
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { handleReaderRequest } from "../../../../lib/reader-api";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
export const runtime = "nodejs";
|
|
6
|
+
|
|
7
|
+
export function POST(request: NextRequest) {
|
|
8
|
+
return handleReaderRequest(request, "submit");
|
|
9
|
+
}
|