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"
|
||||
# Path to a file containing a longer description of your instance
|
||||
# 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
|
||||
# logo = ""
|
||||
# URL to your instance banner
|
||||
|
|
|
|||
|
|
@ -25,4 +25,8 @@ For frontend developers. Please read [the documentation](./frontend.md).
|
|||
|
||||
## 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 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`
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ export const configValidator = z.object({
|
|||
enabled: z.boolean().default(false),
|
||||
}),
|
||||
signups: z.object({
|
||||
tos_url: z.string().min(1).optional(),
|
||||
registration: z.boolean().default(true),
|
||||
rules: z.array(z.string()).default([]),
|
||||
}),
|
||||
|
|
@ -475,6 +474,8 @@ export const configValidator = z.object({
|
|||
name: z.string().min(1).default("Lysand"),
|
||||
description: z.string().min(1).default("A Lysand instance"),
|
||||
extended_description_path: z.string().optional(),
|
||||
tos_path: z.string().optional(),
|
||||
privacy_policy_path: z.string().optional(),
|
||||
logo: zUrl.optional(),
|
||||
banner: zUrl.optional(),
|
||||
})
|
||||
|
|
@ -482,6 +483,8 @@ export const configValidator = z.object({
|
|||
name: "Lysand",
|
||||
description: "A Lysand instance",
|
||||
extended_description_path: undefined,
|
||||
tos_path: undefined,
|
||||
privacy_policy_path: undefined,
|
||||
logo: undefined,
|
||||
banner: undefined,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe(meta.route, () => {
|
|||
|
||||
const json = await response.json();
|
||||
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.
|
||||
content:
|
||||
'<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 { dualLogger } from "@/loggers";
|
||||
import { renderMarkdownInPath } from "@/markdown";
|
||||
import { jsonResponse } from "@/response";
|
||||
import type { Hono } from "hono";
|
||||
import { getMarkdownRenderer } from "~/database/entities/Status";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { LogLevel } from "~/packages/log-manager";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -24,36 +22,14 @@ export default (app: Hono) =>
|
|||
meta.route,
|
||||
auth(meta.auth, meta.permissions),
|
||||
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.",
|
||||
);
|
||||
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({
|
||||
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