mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(api): ✨ Add TOS and Privacy Policy support
This commit is contained in:
parent
e9e33432c2
commit
ffcf01e3cd
|
|
@ -306,7 +306,13 @@ name = "Lysand"
|
||||||
description = "A Lysand instance"
|
description = "A Lysand instance"
|
||||||
# Path to a file containing a longer description of your instance
|
# Path to a file containing a longer description of your instance
|
||||||
# This will be parsed as Markdown
|
# This will be parsed as Markdown
|
||||||
# extended_description_path = "config/description.md"
|
# extended_description_path = "config/extended_description.md"
|
||||||
|
# Path to a file containing the terms of service of your instance
|
||||||
|
# This will be parsed as Markdown
|
||||||
|
# tos_path = "config/tos.md"
|
||||||
|
# Path to a file containing the privacy policy of your instance
|
||||||
|
# This will be parsed as Markdown
|
||||||
|
# privacy_policy_path = "config/privacy_policy.md"
|
||||||
# URL to your instance logo
|
# URL to your instance logo
|
||||||
# logo = ""
|
# logo = ""
|
||||||
# URL to your instance banner
|
# URL to your instance banner
|
||||||
|
|
|
||||||
|
|
@ -26,3 +26,7 @@ For frontend developers. Please read [the documentation](./frontend.md).
|
||||||
## Mastodon API Extensions
|
## Mastodon API Extensions
|
||||||
|
|
||||||
Extra attributes have been added to some Mastodon API routes. Those changes are [documented in this document](./mastodon.md)
|
Extra attributes have been added to some Mastodon API routes. Those changes are [documented in this document](./mastodon.md)
|
||||||
|
|
||||||
|
## Instance API
|
||||||
|
|
||||||
|
Extra endpoints have been added to the API to provide additional information about the instance. Please read [the documentation](./instance.md).
|
||||||
11
docs/api/instance.md
Normal file
11
docs/api/instance.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Instance Endpoints
|
||||||
|
|
||||||
|
Extra endpoints have been added to the API to provide additional information about the instance.
|
||||||
|
|
||||||
|
## `/api/v1/instance/tos`
|
||||||
|
|
||||||
|
Returns the same output as Mastodon's `/api/v1/instance/extended_description`, but with the instance's Terms of Service. Configurable at `instance.tos_path` in config.
|
||||||
|
|
||||||
|
## `/api/v1/instance/privacy_policy`
|
||||||
|
|
||||||
|
Returns the same output as Mastodon's `/api/v1/instance/extended_description`, but with the instance's Privacy Policy. Configurable at `instance.privacy_policy_path` in config.
|
||||||
|
|
@ -30,7 +30,7 @@ The URL of the instance's banner image. `null` if there is no banner set.
|
||||||
|
|
||||||
The version of the Lysand instance.
|
The version of the Lysand instance.
|
||||||
|
|
||||||
The normal `version` field is always set to `"4.3.0+glitch"` or similar to not confuse clients that expect a Mastodon instance.
|
The normal `version` field is always set to `"4.3.0+glitch"` or similar, to not confuse clients that expect a Mastodon instance.
|
||||||
|
|
||||||
### `sso`
|
### `sso`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ export const configValidator = z.object({
|
||||||
enabled: z.boolean().default(false),
|
enabled: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
signups: z.object({
|
signups: z.object({
|
||||||
tos_url: z.string().min(1).optional(),
|
|
||||||
registration: z.boolean().default(true),
|
registration: z.boolean().default(true),
|
||||||
rules: z.array(z.string()).default([]),
|
rules: z.array(z.string()).default([]),
|
||||||
}),
|
}),
|
||||||
|
|
@ -475,6 +474,8 @@ export const configValidator = z.object({
|
||||||
name: z.string().min(1).default("Lysand"),
|
name: z.string().min(1).default("Lysand"),
|
||||||
description: z.string().min(1).default("A Lysand instance"),
|
description: z.string().min(1).default("A Lysand instance"),
|
||||||
extended_description_path: z.string().optional(),
|
extended_description_path: z.string().optional(),
|
||||||
|
tos_path: z.string().optional(),
|
||||||
|
privacy_policy_path: z.string().optional(),
|
||||||
logo: zUrl.optional(),
|
logo: zUrl.optional(),
|
||||||
banner: zUrl.optional(),
|
banner: zUrl.optional(),
|
||||||
})
|
})
|
||||||
|
|
@ -482,6 +483,8 @@ export const configValidator = z.object({
|
||||||
name: "Lysand",
|
name: "Lysand",
|
||||||
description: "A Lysand instance",
|
description: "A Lysand instance",
|
||||||
extended_description_path: undefined,
|
extended_description_path: undefined,
|
||||||
|
tos_path: undefined,
|
||||||
|
privacy_policy_path: undefined,
|
||||||
logo: undefined,
|
logo: undefined,
|
||||||
banner: undefined,
|
banner: undefined,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ describe(meta.route, () => {
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
expect(json).toEqual({
|
expect(json).toEqual({
|
||||||
updated_at: new Date(2024, 0, 0).toISOString(),
|
updated_at: new Date(1970, 0, 0).toISOString(),
|
||||||
// This is a [Lysand](https://lysand.org) server with the default extended description.
|
// This is a [Lysand](https://lysand.org) server with the default extended description.
|
||||||
content:
|
content:
|
||||||
'<p>This is a <a href="https://lysand.org">Lysand</a> server with the default extended description.</p>\n',
|
'<p>This is a <a href="https://lysand.org">Lysand</a> server with the default extended description.</p>\n',
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import { applyConfig, auth } from "@/api";
|
import { applyConfig, auth } from "@/api";
|
||||||
import { dualLogger } from "@/loggers";
|
import { renderMarkdownInPath } from "@/markdown";
|
||||||
import { jsonResponse } from "@/response";
|
import { jsonResponse } from "@/response";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import { getMarkdownRenderer } from "~/database/entities/Status";
|
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { LogLevel } from "~/packages/log-manager";
|
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -24,36 +22,14 @@ export default (app: Hono) =>
|
||||||
meta.route,
|
meta.route,
|
||||||
auth(meta.auth, meta.permissions),
|
auth(meta.auth, meta.permissions),
|
||||||
async () => {
|
async () => {
|
||||||
let extended_description = (await getMarkdownRenderer()).render(
|
const { content, lastModified } = await renderMarkdownInPath(
|
||||||
|
config.instance.extended_description_path ?? "",
|
||||||
"This is a [Lysand](https://lysand.org) server with the default extended description.",
|
"This is a [Lysand](https://lysand.org) server with the default extended description.",
|
||||||
);
|
);
|
||||||
let lastModified = new Date(2024, 0, 0);
|
|
||||||
|
|
||||||
const extended_description_file = Bun.file(
|
|
||||||
config.instance.extended_description_path || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await extended_description_file.exists()) {
|
|
||||||
extended_description =
|
|
||||||
(await getMarkdownRenderer()).render(
|
|
||||||
(await extended_description_file
|
|
||||||
.text()
|
|
||||||
.catch(async (e) => {
|
|
||||||
await dualLogger.logError(
|
|
||||||
LogLevel.ERROR,
|
|
||||||
"Routes",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
return "";
|
|
||||||
})) ||
|
|
||||||
"This is a [Lysand](https://lysand.org) server with the default extended description.",
|
|
||||||
) || "";
|
|
||||||
lastModified = new Date(extended_description_file.lastModified);
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
updated_at: lastModified.toISOString(),
|
updated_at: lastModified.toISOString(),
|
||||||
content: extended_description,
|
content,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
23
server/api/api/v1/instance/privacy_policy.test.ts
Normal file
23
server/api/api/v1/instance/privacy_policy.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { config } from "config-manager";
|
||||||
|
import { sendTestRequest } from "~/tests/utils";
|
||||||
|
import { meta } from "./privacy_policy";
|
||||||
|
|
||||||
|
// /api/v1/instance/privacy_policy
|
||||||
|
describe(meta.route, () => {
|
||||||
|
test("should return privacy policy", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(new URL(meta.route, config.http.base_url)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
expect(json).toEqual({
|
||||||
|
updated_at: new Date(1970, 0, 0).toISOString(),
|
||||||
|
// This instance has not provided any privacy policy.
|
||||||
|
content:
|
||||||
|
"<p>This instance has not provided any privacy policy.</p>\n",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
server/api/api/v1/instance/privacy_policy.ts
Normal file
35
server/api/api/v1/instance/privacy_policy.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { applyConfig, auth } from "@/api";
|
||||||
|
import { renderMarkdownInPath } from "@/markdown";
|
||||||
|
import { jsonResponse } from "@/response";
|
||||||
|
import type { Hono } from "hono";
|
||||||
|
import { config } from "~/packages/config-manager";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["GET"],
|
||||||
|
route: "/api/v1/instance/privacy_policy",
|
||||||
|
ratelimits: {
|
||||||
|
max: 300,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default (app: Hono) =>
|
||||||
|
app.on(
|
||||||
|
meta.allowedMethods,
|
||||||
|
meta.route,
|
||||||
|
auth(meta.auth, meta.permissions),
|
||||||
|
async () => {
|
||||||
|
const { content, lastModified } = await renderMarkdownInPath(
|
||||||
|
config.instance.privacy_policy_path ?? "",
|
||||||
|
"This instance has not provided any privacy policy.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonResponse({
|
||||||
|
updated_at: lastModified.toISOString(),
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
23
server/api/api/v1/instance/tos.test.ts
Normal file
23
server/api/api/v1/instance/tos.test.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { config } from "config-manager";
|
||||||
|
import { sendTestRequest } from "~/tests/utils";
|
||||||
|
import { meta } from "./tos";
|
||||||
|
|
||||||
|
// /api/v1/instance/tos
|
||||||
|
describe(meta.route, () => {
|
||||||
|
test("should return terms of service", async () => {
|
||||||
|
const response = await sendTestRequest(
|
||||||
|
new Request(new URL(meta.route, config.http.base_url)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
expect(json).toEqual({
|
||||||
|
updated_at: new Date(1970, 0, 0).toISOString(),
|
||||||
|
// This instance has not provided any terms of service.
|
||||||
|
content:
|
||||||
|
"<p>This instance has not provided any terms of service.</p>\n",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
server/api/api/v1/instance/tos.ts
Normal file
35
server/api/api/v1/instance/tos.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { applyConfig, auth } from "@/api";
|
||||||
|
import { renderMarkdownInPath } from "@/markdown";
|
||||||
|
import { jsonResponse } from "@/response";
|
||||||
|
import type { Hono } from "hono";
|
||||||
|
import { config } from "~/packages/config-manager";
|
||||||
|
|
||||||
|
export const meta = applyConfig({
|
||||||
|
allowedMethods: ["GET"],
|
||||||
|
route: "/api/v1/instance/tos",
|
||||||
|
ratelimits: {
|
||||||
|
max: 300,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default (app: Hono) =>
|
||||||
|
app.on(
|
||||||
|
meta.allowedMethods,
|
||||||
|
meta.route,
|
||||||
|
auth(meta.auth, meta.permissions),
|
||||||
|
async () => {
|
||||||
|
const { content, lastModified } = await renderMarkdownInPath(
|
||||||
|
config.instance.tos_path ?? "",
|
||||||
|
"This instance has not provided any terms of service.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return jsonResponse({
|
||||||
|
updated_at: lastModified.toISOString(),
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
31
utils/markdown.ts
Normal file
31
utils/markdown.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { markdownParse } from "~/database/entities/Status";
|
||||||
|
import { LogLevel } from "~/packages/log-manager";
|
||||||
|
import { dualLogger } from "./loggers";
|
||||||
|
|
||||||
|
export const renderMarkdownInPath = async (
|
||||||
|
path: string,
|
||||||
|
defaultText?: string,
|
||||||
|
) => {
|
||||||
|
let content = await markdownParse(defaultText ?? "");
|
||||||
|
let lastModified = new Date(1970, 0, 0);
|
||||||
|
|
||||||
|
const extended_description_file = Bun.file(path || "");
|
||||||
|
|
||||||
|
if (path && (await extended_description_file.exists())) {
|
||||||
|
content =
|
||||||
|
(await markdownParse(
|
||||||
|
(await extended_description_file.text().catch(async (e) => {
|
||||||
|
await dualLogger.logError(LogLevel.ERROR, "Routes", e);
|
||||||
|
return "";
|
||||||
|
})) ||
|
||||||
|
defaultText ||
|
||||||
|
"",
|
||||||
|
)) || "";
|
||||||
|
lastModified = new Date(extended_description_file.lastModified);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: content,
|
||||||
|
lastModified,
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue