diff --git a/README.md b/README.md index b23635cf..47f174ba 100644 --- a/README.md +++ b/README.md @@ -259,12 +259,13 @@ Tests needed but completed: - `/api/v1/media/:id` - `/api/v1/favourites` +- `/api/v1/accounts/:id/followers` +- `/api/v1/accounts/:id/following` Endpoints left: - `/api/v1/reports` - `/api/v1/accounts/:id/lists` -- `/api/v1/accounts/:id/following` - `/api/v1/follow_requests` - `/api/v1/follow_requests/:account_id/authorize` - `/api/v1/follow_requests/:account_id/reject` @@ -330,126 +331,6 @@ Endpoints left: WebSocket Streaming API also needed to be added (and push notifications) -## Configuration Values - -Configuration can be found inside the `config.toml` file. The following values are available: - -### Database - -- `host`: The hostname or IP address of the database server. Example: `"localhost"` -- `port`: The port number to use for the database connection. Example: `48654` -- `username`: The username to use for the database connection. Example: `"lysand"` -- `password`: The password to use for the database connection. Example: `"mycoolpassword"` -- `database`: The name of the database to use. Example: `"lysand"` - -### HTTP - -- `base_url`: The base URL for the HTTP server. Example: `"https://lysand.social"` -- `bind`: The hostname or IP address to bind the HTTP server to. Example: `"http://localhost"` -- `bind_port`: The port number to bind the HTTP server to. Example: `"8080"` - -#### Security - -- `banned_ips`: An array of strings representing banned IPv4 or IPv6 IPs. Wildcards, networks and ranges are supported. Example: `[ "192.168.0.*" ]` (empty array) - -### Media - -- `backend`: Specifies the backend to use for media storage. Can be "local" or "s3", "local" uploads the file to the local filesystem. -- `deduplicate_media`: When set to true, the hash of media is checked when uploading to avoid duplication. - -#### Conversion - -- `convert_images`: Whether to convert uploaded images to another format. Example: `true` -- `convert_to`: The format to convert uploaded images to. Example: `"webp"`. Can be "jxl", "webp", "avif", "png", "jpg" or "gif". - -### S3 - -- `endpoint`: The endpoint to use for the S3 server. Example: `"https://s3.example.com"` -- `access_key`: Access key to use for S3 -- `secret_access_key`: Secret access key to use for S3 -- `bucket_name`: The bucket to use for S3 (can be left empty) -- `region`: The region to use for S3 (can be left empty) -- `public_url`: The public URL to access uploaded media. Example: `"https://cdn.example.com"` - -### SMTP - -- `server`: The SMTP server to use for sending emails. Example: `"smtp.example.com"` -- `port`: The port number to use for the SMTP server. Example: `465` -- `username`: The username to use for the SMTP server. Example: `"test@example.com"` -- `password`: The password to use for the SMTP server. Example: `"password123"` -- `tls`: Whether to use TLS for the SMTP server. Example: `true` - -### Email - -- `send_on_report`: Whether to send an email to moderators when a report is received. Example: `false` -- `send_on_suspend`: Whether to send an email to moderators when a user is suspended. Example: `true` -- `send_on_unsuspend`: Whether to send an email to moderators when a user is unsuspended. Example: `false` - -### Validation - -- `max_displayname_size`: The maximum size of a user's display name, in characters. Example: `30` -- `max_bio_size`: The maximum size of a user's bio, in characters. Example: `160` -- `max_note_size`: The maximum size of a user's note, in characters. Example: `500` -- `max_avatar_size`: The maximum size of a user's avatar image, in bytes. Example: `1048576` (1 MB) -- `max_header_size`: The maximum size of a user's header image, in bytes. Example: `2097152` (2 MB) -- `max_media_size`: The maximum size of a media attachment, in bytes. Example: `5242880` (5 MB) -- `max_media_attachments`: The maximum number of media attachments allowed per post. Example: `4` -- `max_media_description_size`: The maximum size of a media attachment's description, in characters. Example: `100` -- `max_username_size`: The maximum size of a user's username, in characters. Example: `20` -- `username_blacklist`: An array of strings representing usernames that are not allowed to be used by users. Defaults are from Akkoma. Example: `["admin", "moderator"]` -- `blacklist_tempmail`: Whether to blacklist known temporary email providers. Example: `true` -- `email_blacklist`: Additional email providers to blacklist. Example: `["example.com", "test.com"]` -- `url_scheme_whitelist`: An array of strings representing valid URL schemes. URLs that do not use one of these schemes will be parsed as text. Example: `["http", "https"]` -- `allowed_mime_types`: An array of strings representing allowed MIME types for media attachments. Example: `["image/jpeg", "image/png", "video/mp4"]` - -### Defaults - -- `visibility`: The default visibility for new notes. Example: `"public"` -- `language`: The default language for new notes. Example: `"en"` -- `avatar`: The default avatar URL. Example: `""` (empty string) -- `header`: The default header URL. Example: `""` (empty string) - -### ActivityPub - -> **Note**: These options do nothing and date back to when Lysand had ActivityPub support. They will be removed in a future version. - -- `use_tombstones`: Whether to use ActivityPub Tombstones instead of deleting objects. Example: `true` -- `fetch_all_collection_members`: Whether to fetch all members of collections (followers, following, etc) when receiving them. Example: `false` -- `reject_activities`: An array of instance domain names without "https" or glob patterns. Rejects all activities from these instances, simply doesn't save them at all. Example: `[ "mastodon.social" ]` -- `force_followers_only`: An array of instance domain names without "https" or glob patterns. Force posts from this instance to be followers only. Example: `[ "mastodon.social" ]` -- `discard_reports`: An array of instance domain names without "https" or glob patterns. Discard all reports from these instances. Example: `[ "mastodon.social" ]` -- `discard_deletes`: An array of instance domain names without "https" or glob patterns. Discard all deletes from these instances. Example: `[ "mastodon.social" ]` -- `discard_updates`: An array of instance domain names without "https" or glob patterns. Discard all updates (edits) from these instances. Example: `[]` -- `discard_banners`: An array of instance domain names without "https" or glob patterns. Discard all banners from these instances. Example: `[ "mastodon.social" ]` -- `discard_avatars`: An array of instance domain names without "https" or glob patterns. Discard all avatars from these instances. Example: `[ "mastodon.social" ]` -- `discard_follows`: An array of instance domain names without "https" or glob patterns. Discard all follow requests from these instances. Example: `[]` -- `force_sensitive`: An array of instance domain names without "https" or glob patterns. Force set these instances' media as sensitive. Example: `[ "mastodon.social" ]` -- `remove_media`: An array of instance domain names without "https" or glob patterns. Remove these instances' media. Example: `[ "mastodon.social" ]` - -### Filters - -- `note_filters`: An array of regex filters to drop notes from new activities. Example: `["(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+", "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+"]` -- `username_filters`: An array of regex filters to drop users from new activities based on their username. Example: `[ "^spammer-[a-z]" ]` -- `displayname_filters`: An array of regex filters to drop users from new activities based on their display name. Example: `[ "^spammer-[a-z]" ]` -- `bio_filters`: An array of regex filters to drop users from new activities based on their bio. Example: `[ "badword" ]` -- `emoji_filters`: An array of regex filters to drop users from new activities based on their emoji usage. Example: `[ ":bademoji:" ]` - -### Logging - -- `log_requests`: Whether to log all requests. Example: `true` -- `log_requests_verbose`: Whether to log request and their contents. Example: `false` -- `log_filters`: Whether to log all filtered objects. Example: `true` - -### Ratelimits - -- `duration_coeff`: The amount to multiply every route's duration by. Example: `1.0` -- `max_coeff`: The amount to multiply every route's max by. Example: `1.0` - -### Custom Ratelimits - -- `"/api/v1/timelines/public"`: An object representing a custom ratelimit for the specified API route. Example: `{ duration = 60, max = 200 }` - - ## License This project is licensed under the [AGPL-3.0](LICENSE). \ No newline at end of file diff --git a/server/api/api/v1/accounts/[id]/followers.ts b/server/api/api/v1/accounts/[id]/followers.ts new file mode 100644 index 00000000..af85ffdf --- /dev/null +++ b/server/api/api/v1/accounts/[id]/followers.ts @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { errorResponse, jsonResponse } from "@response"; +import type { MatchedRoute } from "bun"; +import { userRelations, userToAPI } from "~database/entities/User"; +import { applyConfig } from "@api"; +import { client } from "~database/datasource"; + +export const meta = applyConfig({ + allowedMethods: ["GET"], + ratelimits: { + max: 60, + duration: 60, + }, + route: "/accounts/:id/followers", + auth: { + required: false, + }, +}); + +/** + * Fetch all statuses for a user + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const id = matchedRoute.params.id; + + // TODO: Add pinned + const { + max_id, + min_id, + since_id, + limit = 20, + }: { + max_id?: string; + since_id?: string; + min_id?: string; + limit?: number; + } = matchedRoute.query; + + const user = await client.user.findUnique({ + where: { id }, + include: userRelations, + }); + + if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400); + + if (!user) return errorResponse("User not found", 404); + + const objects = await client.user.findMany({ + where: { + relationships: { + some: { + subjectId: user.id, + following: true, + }, + }, + id: { + lt: max_id, + gt: min_id, + gte: since_id, + }, + }, + include: userRelations, + take: Number(limit), + orderBy: { + id: "desc", + }, + }); + + // Constuct HTTP Link header (next and prev) + const linkHeader = []; + if (objects.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + linkHeader.push( + `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, + `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` + ); + } + + return jsonResponse( + await Promise.all(objects.map(object => userToAPI(object))), + 200, + { + Link: linkHeader.join(", "), + } + ); +}; diff --git a/server/api/api/v1/accounts/[id]/following.ts b/server/api/api/v1/accounts/[id]/following.ts new file mode 100644 index 00000000..2f9d8142 --- /dev/null +++ b/server/api/api/v1/accounts/[id]/following.ts @@ -0,0 +1,89 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { errorResponse, jsonResponse } from "@response"; +import type { MatchedRoute } from "bun"; +import { userRelations, userToAPI } from "~database/entities/User"; +import { applyConfig } from "@api"; +import { client } from "~database/datasource"; + +export const meta = applyConfig({ + allowedMethods: ["GET"], + ratelimits: { + max: 60, + duration: 60, + }, + route: "/accounts/:id/following", + auth: { + required: false, + }, +}); + +/** + * Fetch all statuses for a user + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + const id = matchedRoute.params.id; + + // TODO: Add pinned + const { + max_id, + min_id, + since_id, + limit = 20, + }: { + max_id?: string; + since_id?: string; + min_id?: string; + limit?: number; + } = matchedRoute.query; + + const user = await client.user.findUnique({ + where: { id }, + include: userRelations, + }); + + if (limit < 1 || limit > 40) return errorResponse("Invalid limit", 400); + + if (!user) return errorResponse("User not found", 404); + + const objects = await client.user.findMany({ + where: { + relationshipSubjects: { + some: { + ownerId: user.id, + following: true, + }, + }, + id: { + lt: max_id, + gt: min_id, + gte: since_id, + }, + }, + include: userRelations, + take: Number(limit), + orderBy: { + id: "desc", + }, + }); + + // Constuct HTTP Link header (next and prev) + const linkHeader = []; + if (objects.length > 0) { + const urlWithoutQuery = req.url.split("?")[0]; + linkHeader.push( + `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, + `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"` + ); + } + + return jsonResponse( + await Promise.all(objects.map(object => userToAPI(object))), + 200, + { + Link: linkHeader.join(", "), + } + ); +};