From d096ab830c97404b0716b2765fa8edbc4f47a6b5 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 2 Jan 2025 01:29:33 +0100 Subject: [PATCH] feat(api): :sparkles: Add initial Push Notifications support --- CHANGELOG.md | 1 + .../push/subscription/index.delete.schema.ts | 31 + api/api/v1/push/subscription/index.delete.ts | 23 + .../v1/push/subscription/index.get.schema.ts | 85 + api/api/v1/push/subscription/index.get.ts | 21 + .../v1/push/subscription/index.post.schema.ts | 44 + api/api/v1/push/subscription/index.post.ts | 45 + .../v1/push/subscription/index.put.schema.ts | 43 + api/api/v1/push/subscription/index.put.ts | 51 + api/api/v1/push/subscription/index.test.ts | 304 +++ app.ts | 2 +- bun.lockb | Bin 398336 -> 398336 bytes classes/database/pushsubscription.ts | 256 ++ docs/api/roles.md | 1 + drizzle/migrations/0040_good_nocturne.sql | 14 + drizzle/migrations/meta/0040_snapshot.json | 2323 +++++++++++++++++ drizzle/migrations/meta/_journal.json | 7 + drizzle/schema.ts | 45 + package.json | 2 +- packages/plugin-kit/exports/db.ts | 1 + utils/api.ts | 5 +- 21 files changed, 3301 insertions(+), 3 deletions(-) create mode 100644 api/api/v1/push/subscription/index.delete.schema.ts create mode 100644 api/api/v1/push/subscription/index.delete.ts create mode 100644 api/api/v1/push/subscription/index.get.schema.ts create mode 100644 api/api/v1/push/subscription/index.get.ts create mode 100644 api/api/v1/push/subscription/index.post.schema.ts create mode 100644 api/api/v1/push/subscription/index.post.ts create mode 100644 api/api/v1/push/subscription/index.put.schema.ts create mode 100644 api/api/v1/push/subscription/index.put.ts create mode 100644 api/api/v1/push/subscription/index.test.ts create mode 100644 classes/database/pushsubscription.ts create mode 100644 drizzle/migrations/0040_good_nocturne.sql create mode 100644 drizzle/migrations/meta/0040_snapshot.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b67c27d2..ff23fd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Versia Server `0.8.0` is fully backwards compatible with `0.7.0`. - Outbound federation, inbox processing and data fetching are now handled by a queue system (like most federated software). - Added an administration UI for managing the queue. +- Added [Push Notifications](https://docs.joinmastodon.org/methods/push) support. - Upgraded Bun to `1.1.42`. - Implemented support for the [**Instance Messaging Extension**](https://versia.pub/extensions/instance-messaging) - Implement [**Shared Inboxes**](https://versia.pub/federation#inboxes) support. diff --git a/api/api/v1/push/subscription/index.delete.schema.ts b/api/api/v1/push/subscription/index.delete.schema.ts new file mode 100644 index 00000000..4e274f14 --- /dev/null +++ b/api/api/v1/push/subscription/index.delete.schema.ts @@ -0,0 +1,31 @@ +import { auth } from "@/api"; +import { createRoute, z } from "@hono/zod-openapi"; +import { RolePermissions } from "~/drizzle/schema"; + +export const route = createRoute({ + method: "delete", + path: "/api/v1/push/subscription", + summary: "Remove current subscription", + description: "Removes the current Web Push API subscription.", + externalDocs: { + url: "https://docs.joinmastodon.org/methods/push/#delete", + }, + middleware: [ + auth({ + auth: true, + permissions: [RolePermissions.UsePushNotifications], + scopes: ["push"], + }), + ] as const, + responses: { + 200: { + description: + "PushSubscription successfully deleted or did not exist previously.", + content: { + "application/json": { + schema: z.object({}), + }, + }, + }, + }, +}); diff --git a/api/api/v1/push/subscription/index.delete.ts b/api/api/v1/push/subscription/index.delete.ts new file mode 100644 index 00000000..9150bc88 --- /dev/null +++ b/api/api/v1/push/subscription/index.delete.ts @@ -0,0 +1,23 @@ +import { apiRoute } from "@/api"; +import { PushSubscription } from "@versia/kit/db"; +import { ApiError } from "~/classes/errors/api-error"; +import { route } from "./index.delete.schema"; + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { token } = context.get("auth"); + + const ps = await PushSubscription.fromToken(token); + + if (!ps) { + throw new ApiError( + 404, + "No push subscription associated with this access token", + ); + } + + await ps.delete(); + + return context.json({}, 200); + }), +); diff --git a/api/api/v1/push/subscription/index.get.schema.ts b/api/api/v1/push/subscription/index.get.schema.ts new file mode 100644 index 00000000..7032a288 --- /dev/null +++ b/api/api/v1/push/subscription/index.get.schema.ts @@ -0,0 +1,85 @@ +import { auth } from "@/api"; +import { createRoute, z } from "@hono/zod-openapi"; +import { PushSubscription } from "@versia/kit/db"; +import { RolePermissions } from "~/drizzle/schema"; + +export const WebPushSubscriptionInput = z + .object({ + subscription: z.object({ + endpoint: z.string().url().openapi({ + example: "https://yourdomain.example/listener", + description: "Where push alerts will be sent to.", + }), + keys: z + .object({ + p256dh: z.string().base64().openapi({ + description: + "User agent public key. Base64 encoded string of a public key from a ECDH keypair using the prime256v1 curve.", + example: + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoKCJeHCy69ywHcb3dAR/T8Sud5ljSFHJkuiR6it1ycqAjGTe5F1oZ0ef5QiMX/zdQ+d4jSKiO7RztIz+o/eGuQ==", + }), + auth: z.string().base64().length(24).openapi({ + description: + "Auth secret. Base64 encoded string of 16 bytes of random data.", + example: "u67u09PXZW4ncK9l9mAXkA==", + }), + }) + .strict(), + }), + data: z + .object({ + policy: z + .enum(["all", "followed", "follower", "none"]) + .default("all") + .openapi({ + description: + "Specify whether to receive push notifications from all, followed, follower, or none users.", + }), + alerts: PushSubscription.schema.shape.alerts, + }) + .strict() + .default({ + policy: "all", + alerts: { + mention: false, + favourite: false, + reblog: false, + follow: false, + poll: false, + follow_request: false, + status: false, + update: false, + "admin.sign_up": false, + "admin.report": false, + }, + }), + }) + .strict(); + +export const route = createRoute({ + method: "get", + path: "/api/v1/push/subscription", + summary: "Get current subscription", + description: + "View the PushSubscription currently associated with this access token.", + externalDocs: { + url: "https://docs.joinmastodon.org/methods/push/#get", + }, + middleware: [ + auth({ + auth: true, + permissions: [RolePermissions.UsePushNotifications], + scopes: ["push"], + }), + ] as const, + responses: { + 200: { + description: "WebPushSubscription", + content: { + "application/json": { + schema: PushSubscription.schema, + }, + }, + }, + }, +}); diff --git a/api/api/v1/push/subscription/index.get.ts b/api/api/v1/push/subscription/index.get.ts new file mode 100644 index 00000000..22e11c20 --- /dev/null +++ b/api/api/v1/push/subscription/index.get.ts @@ -0,0 +1,21 @@ +import { apiRoute } from "@/api"; +import { PushSubscription } from "@versia/kit/db"; +import { ApiError } from "~/classes/errors/api-error"; +import { route } from "./index.get.schema"; + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { token } = context.get("auth"); + + const ps = await PushSubscription.fromToken(token); + + if (!ps) { + throw new ApiError( + 404, + "No push subscription associated with this access token", + ); + } + + return context.json(ps.toApi(), 200); + }), +); diff --git a/api/api/v1/push/subscription/index.post.schema.ts b/api/api/v1/push/subscription/index.post.schema.ts new file mode 100644 index 00000000..e3cce240 --- /dev/null +++ b/api/api/v1/push/subscription/index.post.schema.ts @@ -0,0 +1,44 @@ +import { auth, jsonOrForm } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; +import { PushSubscription } from "@versia/kit/db"; +import { RolePermissions } from "~/drizzle/schema"; +import { WebPushSubscriptionInput } from "./index.get.schema"; + +export const route = createRoute({ + method: "post", + path: "/api/v1/push/subscription", + summary: "Subscribe to push notifications", + description: + "Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.", + externalDocs: { + url: "https://docs.joinmastodon.org/methods/push/#create", + }, + middleware: [ + auth({ + auth: true, + permissions: [RolePermissions.UsePushNotifications], + scopes: ["push"], + }), + jsonOrForm(), + ] as const, + request: { + body: { + content: { + "application/json": { + schema: WebPushSubscriptionInput, + }, + }, + }, + }, + responses: { + 200: { + description: + "A new PushSubscription has been generated, which will send the requested alerts to your endpoint.", + content: { + "application/json": { + schema: PushSubscription.schema, + }, + }, + }, + }, +}); diff --git a/api/api/v1/push/subscription/index.post.ts b/api/api/v1/push/subscription/index.post.ts new file mode 100644 index 00000000..af9f7189 --- /dev/null +++ b/api/api/v1/push/subscription/index.post.ts @@ -0,0 +1,45 @@ +import { apiRoute } from "@/api"; +import { PushSubscription } from "@versia/kit/db"; +import { ApiError } from "~/classes/errors/api-error"; +import { RolePermissions } from "~/drizzle/schema"; +import { route } from "./index.post.schema"; + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { user, token } = context.get("auth"); + const { subscription, data } = context.req.valid("json"); + + if ( + data.alerts["admin.report"] && + !user.hasPermission(RolePermissions.ManageReports) + ) { + throw new ApiError( + 403, + `You do not have the '${RolePermissions.ManageReports}' permission to receive report alerts`, + ); + } + + if ( + data.alerts["admin.sign_up"] && + !user.hasPermission(RolePermissions.ManageAccounts) + ) { + throw new ApiError( + 403, + `You do not have the '${RolePermissions.ManageAccounts}' permission to receive sign-up alerts`, + ); + } + + await PushSubscription.clearAllOfToken(token); + + const ps = await PushSubscription.insert({ + alerts: data.alerts, + policy: data.policy, + endpoint: subscription.endpoint, + publicKey: subscription.keys.p256dh, + authSecret: subscription.keys.auth, + tokenId: token.id, + }); + + return context.json(ps.toApi(), 200); + }), +); diff --git a/api/api/v1/push/subscription/index.put.schema.ts b/api/api/v1/push/subscription/index.put.schema.ts new file mode 100644 index 00000000..f9b0a504 --- /dev/null +++ b/api/api/v1/push/subscription/index.put.schema.ts @@ -0,0 +1,43 @@ +import { auth, jsonOrForm } from "@/api"; +import { createRoute } from "@hono/zod-openapi"; +import { PushSubscription } from "@versia/kit/db"; +import { RolePermissions } from "~/drizzle/schema"; +import { WebPushSubscriptionInput } from "./index.get.schema"; + +export const route = createRoute({ + method: "put", + path: "/api/v1/push/subscription", + summary: "Change types of notifications", + description: + "Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.", + externalDocs: { + url: "https://docs.joinmastodon.org/methods/push/#update", + }, + middleware: [ + auth({ + auth: true, + permissions: [RolePermissions.UsePushNotifications], + scopes: ["push"], + }), + jsonOrForm(), + ] as const, + request: { + body: { + content: { + "application/json": { + schema: WebPushSubscriptionInput.shape.data, + }, + }, + }, + }, + responses: { + 200: { + description: "The WebPushSubscription has been updated.", + content: { + "application/json": { + schema: PushSubscription.schema, + }, + }, + }, + }, +}); diff --git a/api/api/v1/push/subscription/index.put.ts b/api/api/v1/push/subscription/index.put.ts new file mode 100644 index 00000000..65579ad8 --- /dev/null +++ b/api/api/v1/push/subscription/index.put.ts @@ -0,0 +1,51 @@ +import { apiRoute } from "@/api"; +import { PushSubscription } from "@versia/kit/db"; +import { ApiError } from "~/classes/errors/api-error"; +import { RolePermissions } from "~/drizzle/schema"; +import { route } from "./index.put.schema"; + +export default apiRoute((app) => + app.openapi(route, async (context) => { + const { user, token } = context.get("auth"); + const { alerts, policy } = context.req.valid("json"); + + const ps = await PushSubscription.fromToken(token); + + if (!ps) { + throw new ApiError( + 404, + "No push subscription associated with this access token", + ); + } + + if ( + alerts["admin.report"] && + !user.hasPermission(RolePermissions.ManageReports) + ) { + throw new ApiError( + 403, + `You do not have the '${RolePermissions.ManageReports}' permission to receive report alerts`, + ); + } + + if ( + alerts["admin.sign_up"] && + !user.hasPermission(RolePermissions.ManageAccounts) + ) { + throw new ApiError( + 403, + `You do not have the '${RolePermissions.ManageAccounts}' permission to receive sign-up alerts`, + ); + } + + await ps.update({ + policy, + alerts: { + ...ps.data.alerts, + ...alerts, + }, + }); + + return context.json(ps.toApi(), 200); + }), +); diff --git a/api/api/v1/push/subscription/index.test.ts b/api/api/v1/push/subscription/index.test.ts new file mode 100644 index 00000000..dd2ec6e7 --- /dev/null +++ b/api/api/v1/push/subscription/index.test.ts @@ -0,0 +1,304 @@ +import { afterAll, beforeEach, describe, expect, test } from "bun:test"; +import { PushSubscription } from "@versia/kit/db"; +import { fakeRequest, getTestUsers } from "~/tests/utils"; + +const { users, tokens, deleteUsers } = await getTestUsers(2); + +afterAll(async () => { + await deleteUsers(); +}); + +beforeEach(async () => { + await PushSubscription.clearAllOfToken(tokens[0]); + await PushSubscription.clearAllOfToken(tokens[1]); +}); + +describe("/api/v1/push/subscriptions", () => { + test("should create a push subscription", async () => { + const res = await fakeRequest("/api/v1/push/subscription", { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].data.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: { + alerts: { + update: true, + }, + policy: "all", + }, + subscription: { + endpoint: "https://example.com", + keys: { + p256dh: "test", + auth: "testthatis24charactersha", + }, + }, + }), + }); + + expect(res.status).toBe(200); + + const body = await res.json(); + + expect(body).toMatchObject({ + endpoint: "https://example.com", + alerts: { + update: true, + }, + }); + }); + + test("should retrieve the same push subscription", async () => { + await PushSubscription.insert({ + endpoint: "https://example.com", + alerts: { + update: true, + "admin.report": false, + "admin.sign_up": false, + favourite: false, + follow: false, + follow_request: false, + mention: false, + poll: false, + reblog: false, + status: false, + }, + policy: "all", + authSecret: "test", + publicKey: "test", + tokenId: tokens[1].id, + }); + + const res = await fakeRequest("/api/v1/push/subscription", { + headers: { + Authorization: `Bearer ${tokens[1].data.accessToken}`, + }, + }); + + expect(res.status).toBe(200); + + const body = await res.json(); + + expect(body).toMatchObject({ + endpoint: "https://example.com", + alerts: { + update: true, + }, + }); + }); + + test("should update a push subscription", async () => { + await PushSubscription.insert({ + endpoint: "https://example.com", + alerts: { + update: true, + "admin.report": false, + "admin.sign_up": false, + favourite: false, + follow: false, + follow_request: false, + mention: false, + poll: false, + reblog: false, + status: false, + }, + policy: "all", + authSecret: "test", + publicKey: "test", + tokenId: tokens[0].id, + }); + + const res = await fakeRequest("/api/v1/push/subscription", { + method: "PUT", + headers: { + Authorization: `Bearer ${tokens[0].data.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + alerts: { + update: false, + favourite: true, + }, + policy: "follower", + }), + }); + + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ + alerts: { + update: false, + favourite: true, + }, + }); + }); + + describe("permissions", () => { + test("should not allow watching admin reports without permissions", async () => { + const res = await fakeRequest("/api/v1/push/subscription", { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].data.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: { + alerts: { + "admin.report": true, + }, + policy: "all", + }, + subscription: { + endpoint: "https://example.com", + keys: { + p256dh: "test", + auth: "testthatis24charactersha", + }, + }, + }), + }); + + expect(res.status).toBe(403); + expect(await res.json()).toMatchObject({ + error: expect.stringContaining("permission"), + }); + }); + + test("should allow watching admin reports with permissions", async () => { + await users[0].update({ + isAdmin: true, + }); + + const res = await fakeRequest("/api/v1/push/subscription", { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].data.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: { + alerts: { + "admin.report": true, + }, + policy: "all", + }, + subscription: { + endpoint: "https://example.com", + keys: { + p256dh: "test", + auth: "testthatis24charactersha", + }, + }, + }), + }); + + expect(res.status).toBe(200); + + await users[0].update({ + isAdmin: false, + }); + }); + + test("should not allow editing to add admin reports without permissions", async () => { + await PushSubscription.insert({ + endpoint: "https://example.com", + alerts: { + update: true, + "admin.report": false, + "admin.sign_up": false, + favourite: false, + follow: false, + follow_request: false, + mention: false, + poll: false, + reblog: false, + status: false, + }, + policy: "all", + authSecret: "test", + publicKey: "test", + tokenId: tokens[0].id, + }); + + const res = await fakeRequest("/api/v1/push/subscription", { + method: "PUT", + headers: { + Authorization: `Bearer ${tokens[0].data.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + alerts: { + "admin.report": true, + }, + policy: "all", + }), + }); + + expect(res.status).toBe(403); + expect(await res.json()).toMatchObject({ + error: expect.stringContaining("permission"), + }); + }); + + test("should allow editing to add admin reports with permissions", async () => { + await users[0].update({ + isAdmin: true, + }); + + await PushSubscription.insert({ + endpoint: "https://example.com", + alerts: { + update: true, + "admin.report": false, + "admin.sign_up": false, + favourite: false, + follow: false, + follow_request: false, + mention: false, + poll: false, + reblog: false, + status: false, + }, + policy: "all", + authSecret: "test", + publicKey: "test", + tokenId: tokens[0].id, + }); + + const res = await fakeRequest("/api/v1/push/subscription", { + method: "PUT", + headers: { + Authorization: `Bearer ${tokens[0].data.accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + alerts: { + "admin.report": true, + }, + policy: "all", + }), + }); + + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ + alerts: { + update: true, + "admin.report": true, + "admin.sign_up": false, + favourite: false, + follow: false, + follow_request: false, + mention: false, + poll: false, + reblog: false, + status: false, + }, + }); + + await users[0].update({ + isAdmin: false, + }); + }); + }); +}); diff --git a/app.ts b/app.ts index 9b53458c..94076e40 100644 --- a/app.ts +++ b/app.ts @@ -110,7 +110,7 @@ export const appFactory = async (): Promise> => { const route: ApiRouteExports = await import(path); if (!route.default) { - throw new Error(`Route ${path} does not have the correct exports.`); + continue; } route.default(app); diff --git a/bun.lockb b/bun.lockb index 8d2ca8a2dea1ac92701a2fbfab5eef119cde953d..e1af2de5f6b536e6f9aaa84ae88eebf961be662b 100755 GIT binary patch delta 173 zcmV;e08;;es2G5#7?3U?y4Hw|fsMh=qAl*w=8B$i>K|n3Ks4Rht>ae%fM*}xu}<20 zlcnN)}oYiCDX-J3Fya9*RyaKn?yaS#y0X4UD@B^$Rm(cVB b1ebCa1R94v76i9G76r2o0X4VsBn1xdL8(wm delta 172 zcmV;d08{^fs2G5#7?3U?kbWvcROxb!p}Nu2TCV-C|C4x6($`D^)t-h8S{_Pyu}<20 zljuM&gIIgFSbG6IK|pZ~QeyNh2RctIKxM4@bt(wUbddmK6iKJ<;?YO0_yZ9l_; + +export class PushSubscription extends BaseInterface< + typeof PushSubscriptions, + PushSubscriptionType +> { + public static schema = z.object({ + id: z.string().uuid().openapi({ + example: "24eb1891-accc-43b4-b213-478e37d525b4", + description: "The ID of the Web Push subscription in the database.", + }), + endpoint: z.string().url().openapi({ + example: "https://yourdomain.example/listener", + description: "Where push alerts will be sent to.", + }), + alerts: z + .object({ + mention: z.boolean().optional().openapi({ + example: true, + description: "Receive mention notifications?", + }), + favourite: z.boolean().optional().openapi({ + example: true, + description: "Receive favourite notifications?", + }), + reblog: z.boolean().optional().openapi({ + example: true, + description: "Receive reblog notifications?", + }), + follow: z.boolean().optional().openapi({ + example: true, + description: "Receive follow notifications?", + }), + poll: z.boolean().optional().openapi({ + example: false, + description: "Receive poll notifications?", + }), + follow_request: z.boolean().optional().openapi({ + example: false, + description: "Receive follow request notifications?", + }), + status: z.boolean().optional().openapi({ + example: false, + description: + "Receive new subscribed account notifications?", + }), + update: z.boolean().optional().openapi({ + example: false, + description: "Receive status edited notifications?", + }), + "admin.sign_up": z.boolean().optional().openapi({ + example: false, + description: + "Receive new user signup notifications? Must have a role with the appropriate permissions.", + }), + "admin.report": z.boolean().optional().openapi({ + example: false, + description: + "Receive new report notifications? Must have a role with the appropriate permissions.", + }), + }) + .default({}) + .openapi({ + example: { + mention: true, + favourite: true, + reblog: true, + follow: true, + poll: false, + follow_request: false, + status: false, + update: false, + "admin.sign_up": false, + "admin.report": false, + }, + description: + "Which alerts should be delivered to the endpoint.", + }), + server_key: z.string().openapi({ + example: + "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=", + description: "The streaming server’s VAPID key.", + }), + }); + + public static $type: PushSubscriptionType; + + public async reload(): Promise { + const reloaded = await PushSubscription.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload subscription"); + } + + this.data = reloaded.data; + } + + public static async fromId( + id: string | null, + ): Promise { + if (!id) { + return null; + } + + return await PushSubscription.fromSql(eq(PushSubscriptions.id, id)); + } + + public static async fromIds(ids: string[]): Promise { + return await PushSubscription.manyFromSql( + inArray(PushSubscriptions.id, ids), + ); + } + + public static async fromToken( + token: Token, + ): Promise { + return await PushSubscription.fromSql( + eq(PushSubscriptions.tokenId, token.id), + ); + } + + public static async fromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(PushSubscriptions.id), + ): Promise { + const found = await db.query.PushSubscriptions.findFirst({ + where: sql, + orderBy, + }); + + if (!found) { + return null; + } + return new PushSubscription(found); + } + + public static async manyFromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(PushSubscriptions.id), + limit?: number, + offset?: number, + extra?: Parameters[0], + ): Promise { + const found = await db.query.PushSubscriptions.findMany({ + where: sql, + orderBy, + limit, + offset, + with: extra?.with, + }); + + return found.map((s) => new PushSubscription(s)); + } + + public async update( + newSubscription: Partial, + ): Promise { + await db + .update(PushSubscriptions) + .set(newSubscription) + .where(eq(PushSubscriptions.id, this.id)); + + const updated = await PushSubscription.fromId(this.data.id); + + if (!updated) { + throw new Error("Failed to update subscription"); + } + + this.data = updated.data; + return updated.data; + } + + public save(): Promise { + return this.update(this.data); + } + + public static async clearAllOfToken(token: Token): Promise { + await db + .delete(PushSubscriptions) + .where(eq(PushSubscriptions.tokenId, token.id)); + } + + public async delete(ids?: string[]): Promise { + if (Array.isArray(ids)) { + await db + .delete(PushSubscriptions) + .where(inArray(PushSubscriptions.id, ids)); + } else { + await db + .delete(PushSubscriptions) + .where(eq(PushSubscriptions.id, this.id)); + } + } + + public static async insert( + data: InferInsertModel, + ): Promise { + const inserted = ( + await db.insert(PushSubscriptions).values(data).returning() + )[0]; + + const subscription = await PushSubscription.fromId(inserted.id); + + if (!subscription) { + throw new Error("Failed to insert subscription"); + } + + return subscription; + } + + public get id(): string { + return this.data.id; + } + + public getAlerts(): Alerts { + return { + mention: this.data.alerts.mention ?? false, + favourite: this.data.alerts.favourite ?? false, + reblog: this.data.alerts.reblog ?? false, + follow: this.data.alerts.follow ?? false, + poll: this.data.alerts.poll ?? false, + follow_request: this.data.alerts.follow_request ?? false, + status: this.data.alerts.status ?? false, + update: this.data.alerts.update ?? false, + "admin.sign_up": this.data.alerts["admin.sign_up"] ?? false, + "admin.report": this.data.alerts["admin.report"] ?? false, + }; + } + + public toApi(): ApiPushSubscription { + return { + id: this.data.id, + alerts: this.getAlerts(), + endpoint: this.data.endpoint, + // FIXME: Add real key + server_key: "", + }; + } +} diff --git a/docs/api/roles.md b/docs/api/roles.md index 2138e488..27a28e33 100644 --- a/docs/api/roles.md +++ b/docs/api/roles.md @@ -61,6 +61,7 @@ ManageFollows: "follows", ManageOwnFollows: "owner:follow", ManageOwnApps: "owner:app", Search: "search", +UsePushNotifications: "push_notifications", ViewPublicTimelines: "public_timelines", ViewPrimateTimelines: "private_timelines", IgnoreRateLimits: "ignore_rate_limits", diff --git a/drizzle/migrations/0040_good_nocturne.sql b/drizzle/migrations/0040_good_nocturne.sql new file mode 100644 index 00000000..bf052901 --- /dev/null +++ b/drizzle/migrations/0040_good_nocturne.sql @@ -0,0 +1,14 @@ +CREATE TABLE "PushSubscriptions" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "endpoint" text NOT NULL, + "public_key" text NOT NULL, + "auth_secret" text NOT NULL, + "alerts" jsonb NOT NULL, + "policy" text NOT NULL, + "created_at" timestamp(3) DEFAULT now() NOT NULL, + "updated_at" timestamp(3) DEFAULT now() NOT NULL, + "tokenId" uuid NOT NULL, + CONSTRAINT "PushSubscriptions_tokenId_unique" UNIQUE("tokenId") +); +--> statement-breakpoint +ALTER TABLE "PushSubscriptions" ADD CONSTRAINT "PushSubscriptions_tokenId_Tokens_id_fk" FOREIGN KEY ("tokenId") REFERENCES "public"."Tokens"("id") ON DELETE cascade ON UPDATE cascade; \ No newline at end of file diff --git a/drizzle/migrations/meta/0040_snapshot.json b/drizzle/migrations/meta/0040_snapshot.json new file mode 100644 index 00000000..d47166ed --- /dev/null +++ b/drizzle/migrations/meta/0040_snapshot.json @@ -0,0 +1,2323 @@ +{ + "id": "7dcdf012-b727-41b2-be3c-d064e5b04501", + "prevId": "5d0b3e29-18dc-4202-af7d-252de239e7cb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Applications": { + "name": "Applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vapid_key": { + "name": "vapid_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Applications_client_id_index": { + "name": "Applications_client_id_index", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Attachments": { + "name": "Attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fps": { + "name": "fps", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Attachments_noteId_Notes_id_fk": { + "name": "Attachments_noteId_Notes_id_fk", + "tableFrom": "Attachments", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Challenges": { + "name": "Challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "challenge": { + "name": "challenge", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "NOW() + INTERVAL '5 minutes'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmojiToNote": { + "name": "EmojiToNote", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToNote_emojiId_noteId_index": { + "name": "EmojiToNote_emojiId_noteId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToNote_noteId_index": { + "name": "EmojiToNote_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToNote_emojiId_Emojis_id_fk": { + "name": "EmojiToNote_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToNote_noteId_Notes_id_fk": { + "name": "EmojiToNote_noteId_Notes_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmojiToUser": { + "name": "EmojiToUser", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToUser_emojiId_userId_index": { + "name": "EmojiToUser_emojiId_userId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToUser_userId_index": { + "name": "EmojiToUser_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToUser_emojiId_Emojis_id_fk": { + "name": "EmojiToUser_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToUser_userId_Users_id_fk": { + "name": "EmojiToUser_userId_Users_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Emojis": { + "name": "Emojis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Emojis_instanceId_Instances_id_fk": { + "name": "Emojis_instanceId_Instances_id_fk", + "tableFrom": "Emojis", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Emojis_ownerId_Users_id_fk": { + "name": "Emojis_ownerId_Users_id_fk", + "tableFrom": "Emojis", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.FilterKeywords": { + "name": "FilterKeywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "filterId": { + "name": "filterId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whole_word": { + "name": "whole_word", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FilterKeywords_filterId_Filters_id_fk": { + "name": "FilterKeywords_filterId_Filters_id_fk", + "tableFrom": "FilterKeywords", + "tableTo": "Filters", + "columnsFrom": ["filterId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Filters": { + "name": "Filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter_action": { + "name": "filter_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Filters_userId_Users_id_fk": { + "name": "Filters_userId_Users_id_fk", + "tableFrom": "Filters", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Flags": { + "name": "Flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flags_noteId_Notes_id_fk": { + "name": "Flags_noteId_Notes_id_fk", + "tableFrom": "Flags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flags_userId_Users_id_fk": { + "name": "Flags_userId_Users_id_fk", + "tableFrom": "Flags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Instances": { + "name": "Instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'versia'" + }, + "inbox": { + "name": "inbox", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Likes": { + "name": "Likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Likes_likerId_Users_id_fk": { + "name": "Likes_likerId_Users_id_fk", + "tableFrom": "Likes", + "tableTo": "Users", + "columnsFrom": ["likerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Likes_likedId_Notes_id_fk": { + "name": "Likes_likedId_Notes_id_fk", + "tableFrom": "Likes", + "tableTo": "Notes", + "columnsFrom": ["likedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Likes_uri_unique": { + "name": "Likes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Markers": { + "name": "Markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notificationId": { + "name": "notificationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Markers_noteId_Notes_id_fk": { + "name": "Markers_noteId_Notes_id_fk", + "tableFrom": "Markers", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_notificationId_Notifications_id_fk": { + "name": "Markers_notificationId_Notifications_id_fk", + "tableFrom": "Markers", + "tableTo": "Notifications", + "columnsFrom": ["notificationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_userId_Users_id_fk": { + "name": "Markers_userId_Users_id_fk", + "tableFrom": "Markers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ModNotes": { + "name": "ModNotes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNotes_noteId_Notes_id_fk": { + "name": "ModNotes_noteId_Notes_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_userId_Users_id_fk": { + "name": "ModNotes_userId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_modId_Users_id_fk": { + "name": "ModNotes_modId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ModTags": { + "name": "ModTags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTags_noteId_Notes_id_fk": { + "name": "ModTags_noteId_Notes_id_fk", + "tableFrom": "ModTags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_userId_Users_id_fk": { + "name": "ModTags_userId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_modId_Users_id_fk": { + "name": "ModTags_modId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.NoteToMentions": { + "name": "NoteToMentions", + "schema": "", + "columns": { + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "NoteToMentions_noteId_userId_index": { + "name": "NoteToMentions_noteId_userId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "NoteToMentions_userId_index": { + "name": "NoteToMentions_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "NoteToMentions_noteId_Notes_id_fk": { + "name": "NoteToMentions_noteId_Notes_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "NoteToMentions_userId_Users_id_fk": { + "name": "NoteToMentions_userId_Users_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Notes": { + "name": "Notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replyId": { + "name": "replyId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quoteId": { + "name": "quoteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "spoiler_text": { + "name": "spoiler_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "Notes_authorId_Users_id_fk": { + "name": "Notes_authorId_Users_id_fk", + "tableFrom": "Notes", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_reblogId_Notes_id_fk": { + "name": "Notes_reblogId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["reblogId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_replyId_Notes_id_fk": { + "name": "Notes_replyId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["replyId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_quoteId_Notes_id_fk": { + "name": "Notes_quoteId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["quoteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_applicationId_Applications_id_fk": { + "name": "Notes_applicationId_Applications_id_fk", + "tableFrom": "Notes", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Notes_uri_unique": { + "name": "Notes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Notifications": { + "name": "Notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dismissed": { + "name": "dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notifications_notifiedId_Users_id_fk": { + "name": "Notifications_notifiedId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["notifiedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_accountId_Users_id_fk": { + "name": "Notifications_accountId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["accountId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_noteId_Notes_id_fk": { + "name": "Notifications_noteId_Notes_id_fk", + "tableFrom": "Notifications", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpenIdAccounts": { + "name": "OpenIdAccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccounts_userId_Users_id_fk": { + "name": "OpenIdAccounts_userId_Users_id_fk", + "tableFrom": "OpenIdAccounts", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpenIdLoginFlows": { + "name": "OpenIdLoginFlows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlows_applicationId_Applications_id_fk": { + "name": "OpenIdLoginFlows_applicationId_Applications_id_fk", + "tableFrom": "OpenIdLoginFlows", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.PushSubscriptions": { + "name": "PushSubscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_secret": { + "name": "auth_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alerts": { + "name": "alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tokenId": { + "name": "tokenId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "PushSubscriptions_tokenId_Tokens_id_fk": { + "name": "PushSubscriptions_tokenId_Tokens_id_fk", + "tableFrom": "PushSubscriptions", + "tableTo": "Tokens", + "columnsFrom": ["tokenId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "PushSubscriptions_tokenId_unique": { + "name": "PushSubscriptions_tokenId_unique", + "nullsNotDistinct": false, + "columns": ["tokenId"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Reaction": { + "name": "Reaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "emoji_text": { + "name": "emoji_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Reaction_emojiId_Emojis_id_fk": { + "name": "Reaction_emojiId_Emojis_id_fk", + "tableFrom": "Reaction", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Reaction_noteId_Notes_id_fk": { + "name": "Reaction_noteId_Notes_id_fk", + "tableFrom": "Reaction", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Reaction_authorId_Users_id_fk": { + "name": "Reaction_authorId_Users_id_fk", + "tableFrom": "Reaction", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Reaction_uri_unique": { + "name": "Reaction_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Relationships": { + "name": "Relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showing_reblogs": { + "name": "showing_reblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting_notifications": { + "name": "muting_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domain_blocking": { + "name": "domain_blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Relationships_ownerId_Users_id_fk": { + "name": "Relationships_ownerId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationships_subjectId_Users_id_fk": { + "name": "Relationships_subjectId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RoleToUsers": { + "name": "RoleToUsers", + "schema": "", + "columns": { + "roleId": { + "name": "roleId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "RoleToUsers_roleId_Roles_id_fk": { + "name": "RoleToUsers_roleId_Roles_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Roles", + "columnsFrom": ["roleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RoleToUsers_userId_Users_id_fk": { + "name": "RoleToUsers_userId_Users_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Roles": { + "name": "Roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Tokens": { + "name": "Tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Tokens_userId_Users_id_fk": { + "name": "Tokens_userId_Users_id_fk", + "tableFrom": "Tokens", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Tokens_applicationId_Applications_id_fk": { + "name": "Tokens_applicationId_Applications_id_fk", + "tableFrom": "Tokens", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.UserToPinnedNotes": { + "name": "UserToPinnedNotes", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UserToPinnedNotes_userId_noteId_index": { + "name": "UserToPinnedNotes_userId_noteId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UserToPinnedNotes_noteId_index": { + "name": "UserToPinnedNotes_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "UserToPinnedNotes_userId_Users_id_fk": { + "name": "UserToPinnedNotes_userId_Users_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "UserToPinnedNotes_noteId_Notes_id_fk": { + "name": "UserToPinnedNotes_noteId_Notes_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Users": { + "name": "Users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verification_token": { + "name": "email_verification_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_reset_token": { + "name": "password_reset_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header": { + "name": "header", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_discoverable": { + "name": "is_discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Users_uri_index": { + "name": "Users_uri_index", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_username_index": { + "name": "Users_username_index", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_email_index": { + "name": "Users_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Users_instanceId_Instances_id_fk": { + "name": "Users_instanceId_Instances_id_fk", + "tableFrom": "Users", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Users_uri_unique": { + "name": "Users_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index 2ae5dceb..74275e40 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -281,6 +281,13 @@ "when": 1734555117380, "tag": "0039_special_serpent_society", "breakpoints": true + }, + { + "idx": 40, + "version": "7", + "when": 1735776034097, + "tag": "0040_good_nocturne", + "breakpoints": true } ] } diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 65c102a9..cb277002 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -67,6 +67,49 @@ export const Emojis = pgTable("Emojis", { category: text("category"), }); +export const PushSubscriptions = pgTable("PushSubscriptions", { + id: id(), + endpoint: text("endpoint").notNull(), + publicKey: text("public_key").notNull(), + authSecret: text("auth_secret").notNull(), + alerts: jsonb("alerts").notNull().$type< + Partial<{ + mention: boolean; + favourite: boolean; + reblog: boolean; + follow: boolean; + poll: boolean; + follow_request: boolean; + status: boolean; + update: boolean; + "admin.sign_up": boolean; + "admin.report": boolean; + }> + >(), + policy: text("policy") + .notNull() + .$type<"all" | "followed" | "follower" | "none">(), + createdAt: createdAt(), + updatedAt: updatedAt(), + tokenId: uuid("tokenId") + .references(() => Tokens.id, { + onDelete: "cascade", + onUpdate: "cascade", + }) + .notNull() + .unique(), +}); + +export const PushSubscriptionsRelations = relations( + PushSubscriptions, + ({ one }) => ({ + token: one(Tokens, { + fields: [PushSubscriptions.tokenId], + references: [Tokens.id], + }), + }), +); + export const Reactions = pgTable("Reaction", { id: id(), uri: uri(), @@ -548,6 +591,7 @@ export enum RolePermissions { ManageOwnFollows = "owner:follow", ManageOwnApps = "owner:app", Search = "search", + UsePushNotifications = "push_notifications", ViewPublicTimelines = "public_timelines", ViewPrivateTimelines = "private_timelines", IgnoreRateLimits = "ignore_rate_limits", @@ -583,6 +627,7 @@ export const DEFAULT_ROLES = [ RolePermissions.ManageOwnFollows, RolePermissions.ManageOwnApps, RolePermissions.Search, + RolePermissions.UsePushNotifications, RolePermissions.ViewPublicTimelines, RolePermissions.ViewPrivateTimelines, RolePermissions.OAuth, diff --git a/package.json b/package.json index f02e1960..c56be7e3 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "@oclif/core": "^4.2.0", "@sentry/bun": "^8.47.0", "@tufjs/canonical-json": "^2.0.0", - "@versia/client": "^0.1.4", + "@versia/client": "^0.1.5", "@versia/federation": "^0.1.4", "@versia/kit": "workspace:*", "altcha-lib": "^1.2.0", diff --git a/packages/plugin-kit/exports/db.ts b/packages/plugin-kit/exports/db.ts index c6fa2891..1023c7ad 100644 --- a/packages/plugin-kit/exports/db.ts +++ b/packages/plugin-kit/exports/db.ts @@ -13,3 +13,4 @@ export { Like } from "~/classes/database/like.ts"; export { Token } from "~/classes/database/token.ts"; export { Notification } from "~/classes/database/notification.ts"; export { Reaction } from "~/classes/database/reaction.ts"; +export { PushSubscription } from "~/classes/database/pushsubscription.ts"; diff --git a/utils/api.ts b/utils/api.ts index 8cb16283..ecf3608f 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -246,7 +246,10 @@ export const checkRouteNeedsChallenge = async ( type HonoEnvWithAuth = HonoEnv & { Variables: { - auth: AuthData & { user: NonNullable }; + auth: AuthData & { + user: NonNullable; + token: NonNullable; + }; }; };