Add more moderation systems, document new APIs

This commit is contained in:
Jesse Wierzbinski 2024-03-03 18:33:25 -10:00
parent a87c8b6cc5
commit 847e679a10
No known key found for this signature in database
3 changed files with 428 additions and 67 deletions

268
API.md Normal file
View file

@ -0,0 +1,268 @@
# API
The Lysand project uses the Mastodon API to interact with clients. However, the moderation API is custom-made for Lysand Server, as it allows for more fine-grained control over the server's behavior.
## Flags, ModTags and ModNotes
Flags are used by Lysand Server to automatically attribute tags to a status or account based on rules. ModTags and ModNotes are used by moderators to manually tag and take notes on statuses and accounts.
The difference between flags and modtags is that flags are automatically attributed by the server, while modtags are manually attributed by moderators.
### Flag Types
- `content_filter`: (Statuses only) The status contains content that was filtered by the server's content filter.
- `bio_filter`: (Accounts only) The account's bio contains content that was filtered by the server's content filter.
- `emoji_filter`: The status or account contains an emoji that was filtered by the server's content filter.
- `reported`: The status or account was previously reported by a user.
- `suspended`: The status or account was previously suspended by a moderator.
- `silenced`: The status or account was previously silenced by a moderator.
### ModTag Types
ModTag do not have set types and can be anything. Lysand Server autosuggest previously used tags when a moderator is adding a new tag to avoid duplicates.
### Data Format
```ts
type Flag = {
id: string,
// One of the following two fields will be present
flaggedStatus?: Status,
flaggedUser?: User,
flagType: string,
createdAt: string,
}
type ModTag = {
id: string,
// One of the following two fields will be present
taggedStatus?: Status,
taggedUser?: User,
mod: User,
tag: string,
createdAt: string,
}
type ModNote = {
id: string,
// One of the following two fields will be present
notedStatus?: Status,
notedUser?: User,
mod: User,
note: string,
createdAt: string,
}
```
The `User` and `Status` types are the same as the ones in the Mastodon API.
## Moderation API Routes
### `GET /api/v1/moderation/accounts/:id`
Returns full moderation data and flags for the account with the given ID.
Output format:
```ts
{
id: string, // Same ID as in account field
flags: Flag[],
modtags: ModTag[],
modnotes: ModNote[],
account: User,
}
```
### `GET /api/v1/moderation/statuses/:id`
Returns full moderation data and flags for the status with the given ID.
Output format:
```ts
{
id: string, // Same ID as in status field
flags: Flag[],
modtags: ModTag[],
modnotes: ModNote[],
status: Status,
}
```
### `POST /api/v1/moderation/accounts/:id/modtags`
Params:
- `tag`: string
Adds a modtag to the account with the given ID
### `POST /api/v1/moderation/statuses/:id/modtags`
Params:
- `tag`: string
Adds a modtag to the status with the given ID
### `POST /api/v1/moderation/accounts/:id/modnotes`
Params:
- `note`: string
Adds a modnote to the account with the given ID
### `POST /api/v1/moderation/statuses/:id/modnotes`
Params:
- `note`: string
Adds a modnote to the status with the given ID
### `DELETE /api/v1/moderation/accounts/:id/modtags/:modtag_id`
Deletes the modtag with the given ID from the account with the given ID
### `DELETE /api/v1/moderation/statuses/:id/modtags/:modtag_id`
Deletes the modtag with the given ID from the status with the given ID
### `DELETE /api/v1/moderation/accounts/:id/modnotes/:modnote_id`
Deletes the modnote with the given ID from the account with the given ID
### `DELETE /api/v1/moderation/statuses/:id/modnotes/:modnote_id`
Deletes the modnote with the given ID from the status with the given ID
### `GET /api/v1/moderation/modtags`
Returns a list of all modtags previously used by moderators
Output format:
```ts
{
tags: string[],
}
```
### `GET /api/v1/moderation/accounts/flags/search`
Allows moderators to search for accounts based on their flags, this can also include status flags
Params:
- `limit`: Number
- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.
- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results.
- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results.
- `flags`: String (optional). Comma-separated list of flag types to filter by. Can be left out to return accounts with at least one flag
- `flag_count`: Number (optional). Minimum number of flags to filter by
- `include_statuses`: Boolean (optional). If true, includes status flags in the search results
- `account_id`: Array of strings (optional). Filters accounts by account ID
This method returns a `Link` header the same way Mastodon does, to allow for pagination.
Output format:
```ts
{
accounts: {
account: User,
modnotes: ModNote[],
flags: Flag[],
statuses?: {
status: Status,
modnotes: ModNote[],
flags: Flag[],
}[],
}[],
}
```
### `GET /api/v1/moderation/statuses/flags/search`
Allows moderators to search for statuses based on their flags
Params:
- `limit`: Number
- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.
- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results.
- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results.
- `flags`: String (optional). Comma-separated list of flag types to filter by. Can be left out to return statuses with at least one flag
- `flag_count`: Number (optional). Minimum number of flags to filter by
- `account_id`: Array of strings (optional). Filters statuses by account ID
This method returns a `Link` header the same way Mastodon does, to allow for pagination.
Output format:
```ts
{
statuses: {
status: Status,
modnotes: ModNote[],
flags: Flag[],
}[],
}
```
### `GET /api/v1/moderation/accounts/modtags/search`
Allows moderators to search for accounts based on their modtags
Params:
- `limit`: Number
- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.
- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results.
- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results.
- `tags`: String (optional). Comma-separated list of tags to filter by. Can be left out to return accounts with at least one tag
- `tag_count`: Number (optional). Minimum number of tags to filter by
- `include_statuses`: Boolean (optional). If true, includes status tags in the search results
- `account_id`: Array of strings (optional). Filters accounts by account ID
This method returns a `Link` header the same way Mastodon does, to allow for pagination.
Output format:
```ts
{
accounts: {
account: User,
modnotes: ModNote[],
modtags: ModTag[],
statuses?: {
status: Status,
modnotes: ModNote[],
modtags: ModTag[],
}[],
}[],
}
```
### `GET /api/v1/moderation/statuses/modtags/search`
Allows moderators to search for statuses based on their modtags
Params:
- `limit`: Number
- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.
- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results.
- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results.
- `tags`: String (optional). Comma-separated list of tags to filter by. Can be left out to return statuses with at least one tag
- `tag_count`: Number (optional). Minimum number of tags to filter by
- `account_id`: Array of strings (optional). Filters statuses by account ID
- `include_statuses`: Boolean (optional). If true, includes status tags in the search results
This method returns a `Link` header the same way Mastodon does, to allow for pagination.
Output format:
```ts
{
statuses: {
status: Status,
modnotes: ModNote[],
modtags: ModTag[],
}[],
}
```

View file

@ -0,0 +1,76 @@
/*
Warnings:
- You are about to drop the column `statusId` on the `Flag` table. All the data in the column will be lost.
- You are about to drop the column `userId` on the `Flag` table. All the data in the column will be lost.
- You are about to drop the `ModerationData` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "Flag" DROP CONSTRAINT "Flag_statusId_fkey";
-- DropForeignKey
ALTER TABLE "Flag" DROP CONSTRAINT "Flag_userId_fkey";
-- DropForeignKey
ALTER TABLE "ModerationData" DROP CONSTRAINT "ModerationData_creatorId_fkey";
-- DropForeignKey
ALTER TABLE "ModerationData" DROP CONSTRAINT "ModerationData_statusId_fkey";
-- AlterTable
ALTER TABLE "Flag" DROP COLUMN "statusId",
DROP COLUMN "userId",
ADD COLUMN "flaggeStatusId" UUID,
ADD COLUMN "flaggedUserId" UUID;
-- DropTable
DROP TABLE "ModerationData";
-- CreateTable
CREATE TABLE "ModNote" (
"id" UUID NOT NULL DEFAULT uuid_generate_v7(),
"notedStatusId" UUID,
"notedUserId" UUID,
"modId" UUID NOT NULL,
"note" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ModNote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ModTag" (
"id" UUID NOT NULL DEFAULT uuid_generate_v7(),
"taggedStatusId" UUID,
"taggedUserId" UUID,
"modId" UUID NOT NULL,
"tag" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ModTag_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedStatusId_fkey" FOREIGN KEY ("notedStatusId") REFERENCES "Status"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedUserId_fkey" FOREIGN KEY ("notedUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedStatusId_fkey" FOREIGN KEY ("taggedStatusId") REFERENCES "Status"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedUserId_fkey" FOREIGN KEY ("taggedUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_modId_fkey" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggeStatusId_fkey" FOREIGN KEY ("flaggeStatusId") REFERENCES "Status"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggedUserId_fkey" FOREIGN KEY ("flaggedUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -94,62 +94,76 @@ model Relationship {
} }
model Status { model Status {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique uri String @unique
author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade) author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade)
authorId String @db.Uuid authorId String @db.Uuid
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
reblog Status? @relation("StatusToStatus", fields: [reblogId], references: [id], onDelete: Cascade) reblog Status? @relation("StatusToStatus", fields: [reblogId], references: [id], onDelete: Cascade)
reblogId String? @db.Uuid reblogId String? @db.Uuid
isReblog Boolean isReblog Boolean
content String @default("") content String @default("")
contentType String @default("text/plain") contentType String @default("text/plain")
contentSource String @default("") contentSource String @default("")
visibility String visibility String
inReplyToPost Status? @relation("StatusToStatusReply", fields: [inReplyToPostId], references: [id], onDelete: SetNull) inReplyToPost Status? @relation("StatusToStatusReply", fields: [inReplyToPostId], references: [id], onDelete: SetNull)
inReplyToPostId String? @db.Uuid inReplyToPostId String? @db.Uuid
quotingPost Status? @relation("StatusToStatusQuote", fields: [quotingPostId], references: [id], onDelete: SetNull) quotingPost Status? @relation("StatusToStatusQuote", fields: [quotingPostId], references: [id], onDelete: SetNull)
quotingPostId String? @db.Uuid quotingPostId String? @db.Uuid
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String? @db.Uuid instanceId String? @db.Uuid
sensitive Boolean sensitive Boolean
spoilerText String @default("") spoilerText String @default("")
application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull) application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull)
applicationId String? @db.Uuid applicationId String? @db.Uuid
emojis Emoji[] @relation emojis Emoji[] @relation
mentions User[] mentions User[]
likes Like[] @relation("LikedToStatus") likes Like[] @relation("LikedToStatus")
reblogs Status[] @relation("StatusToStatus") reblogs Status[] @relation("StatusToStatus")
replies Status[] @relation("StatusToStatusReply") replies Status[] @relation("StatusToStatusReply")
quotes Status[] @relation("StatusToStatusQuote") quotes Status[] @relation("StatusToStatusQuote")
pinnedBy User[] @relation("UserPinnedNotes") pinnedBy User[] @relation("UserPinnedNotes")
attachments Attachment[] attachments Attachment[]
relatedNotifications Notification[] relatedNotifications Notification[]
flags Flag[] flags Flag[]
moderationData ModerationData[] modNotes ModNote[]
modTags ModTag[]
} }
model ModerationData { model ModNote {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
status Status @relation(fields: [statusId], references: [id], onDelete: Cascade) notedStatus Status? @relation(fields: [notedStatusId], references: [id], onDelete: Cascade)
statusId String @db.Uuid notedStatusId String? @db.Uuid
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade) notedUser User? @relation("ModNoteToUser", fields: [notedUserId], references: [id], onDelete: Cascade)
creatorId String @db.Uuid notedUserId String? @db.Uuid
note String mod User @relation("ModNoteToMod", fields: [modId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) modId String @db.Uuid
updatedAt DateTime @updatedAt note String
createdAt DateTime @default(now())
} }
// Used for moderation purposes model ModTag {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
taggedStatus Status? @relation(fields: [taggedStatusId], references: [id], onDelete: Cascade)
taggedStatusId String? @db.Uuid
taggedUser User? @relation("ModNoteToTaggedUser", fields: [taggedUserId], references: [id], onDelete: Cascade)
taggedUserId String? @db.Uuid
mod User @relation("ModTagToMod", fields: [modId], references: [id], onDelete: Cascade)
modId String @db.Uuid
tag String
createdAt DateTime @default(now())
}
// Used to tag notes and accounts with automatic moderation infractions
model Flag { model Flag {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
status Status @relation(fields: [statusId], references: [id], onDelete: Cascade) flaggedStatus Status? @relation(fields: [flaggeStatusId], references: [id], onDelete: Cascade)
statusId String @db.Uuid flaggeStatusId String? @db.Uuid
user User @relation(fields: [userId], references: [id], onDelete: Cascade) flaggedUser User? @relation(fields: [flaggedUserId], references: [id], onDelete: Cascade)
userId String @db.Uuid flaggedUserId String? @db.Uuid
flagType String @default("other") flagType String @default("other")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
} }
model Token { model Token {
@ -204,42 +218,45 @@ model Notification {
} }
model User { model User {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique uri String @unique
username String @unique username String @unique
displayName String displayName String
password String? // Nullable password String? // Nullable
email String? @unique // Nullable email String? @unique // Nullable
note String @default("") note String @default("")
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
endpoints Json? // Nullable endpoints Json? // Nullable
source Json source Json
avatar String avatar String
header String header String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
isBot Boolean @default(false) isBot Boolean @default(false)
isLocked Boolean @default(false) isLocked Boolean @default(false)
isDiscoverable Boolean @default(false) isDiscoverable Boolean @default(false)
sanctions String[] @default([]) sanctions String[] @default([])
publicKey String publicKey String
privateKey String? // Nullable privateKey String? // Nullable
relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship
relationshipSubjects Relationship[] @relation("SubjectToRelationship") // One to many relation with Relationship relationshipSubjects Relationship[] @relation("SubjectToRelationship") // One to many relation with Relationship
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) // Many to one relation with Instance instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) // Many to one relation with Instance
instanceId String? @db.Uuid instanceId String? @db.Uuid
pinnedNotes Status[] @relation("UserPinnedNotes") // Many to many relation with Status pinnedNotes Status[] @relation("UserPinnedNotes") // Many to many relation with Status
emojis Emoji[] // Many to many relation with Emoji emojis Emoji[] // Many to many relation with Emoji
statuses Status[] @relation("UserStatuses") // One to many relation with Status statuses Status[] @relation("UserStatuses") // One to many relation with Status
tokens Token[] // One to many relation with Token tokens Token[] // One to many relation with Token
likes Like[] @relation("UserLiked") // One to many relation with Like likes Like[] @relation("UserLiked") // One to many relation with Like
statusesMentioned Status[] // Many to many relation with Status statusesMentioned Status[] // Many to many relation with Status
notifications Notification[] // One to many relation with Notification notifications Notification[] // One to many relation with Notification
notified Notification[] @relation("NotificationToNotified") // One to many relation with Notification notified Notification[] @relation("NotificationToNotified") // One to many relation with Notification
linkedOpenIdAccounts OpenIdAccount[] // One to many relation with OpenIdAccount linkedOpenIdAccounts OpenIdAccount[] // One to many relation with OpenIdAccount
flags Flag[] @relation flags Flag[]
moderationData ModerationData[] modNotes ModNote[] @relation("ModNoteToUser")
disableAutomoderation Boolean @default(false) modTags ModTag[] @relation("ModNoteToTaggedUser")
disableAutomoderation Boolean @default(false)
createdModTags ModTag[] @relation("ModTagToMod")
createdModNotes ModNote[] @relation("ModNoteToMod")
} }
model OpenIdAccount { model OpenIdAccount {