mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat: Add following and followers endpoints
This commit is contained in:
parent
d7398e1f5f
commit
45a8a2678e
123
README.md
123
README.md
|
|
@ -259,12 +259,13 @@ Tests needed but completed:
|
||||||
|
|
||||||
- `/api/v1/media/:id`
|
- `/api/v1/media/:id`
|
||||||
- `/api/v1/favourites`
|
- `/api/v1/favourites`
|
||||||
|
- `/api/v1/accounts/:id/followers`
|
||||||
|
- `/api/v1/accounts/:id/following`
|
||||||
|
|
||||||
Endpoints left:
|
Endpoints left:
|
||||||
|
|
||||||
- `/api/v1/reports`
|
- `/api/v1/reports`
|
||||||
- `/api/v1/accounts/:id/lists`
|
- `/api/v1/accounts/:id/lists`
|
||||||
- `/api/v1/accounts/:id/following`
|
|
||||||
- `/api/v1/follow_requests`
|
- `/api/v1/follow_requests`
|
||||||
- `/api/v1/follow_requests/:account_id/authorize`
|
- `/api/v1/follow_requests/:account_id/authorize`
|
||||||
- `/api/v1/follow_requests/:account_id/reject`
|
- `/api/v1/follow_requests/:account_id/reject`
|
||||||
|
|
@ -330,126 +331,6 @@ Endpoints left:
|
||||||
|
|
||||||
WebSocket Streaming API also needed to be added (and push notifications)
|
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
|
## License
|
||||||
|
|
||||||
This project is licensed under the [AGPL-3.0](LICENSE).
|
This project is licensed under the [AGPL-3.0](LICENSE).
|
||||||
89
server/api/api/v1/accounts/[id]/followers.ts
Normal file
89
server/api/api/v1/accounts/[id]/followers.ts
Normal file
|
|
@ -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<Response> => {
|
||||||
|
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(", "),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
89
server/api/api/v1/accounts/[id]/following.ts
Normal file
89
server/api/api/v1/accounts/[id]/following.ts
Normal file
|
|
@ -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<Response> => {
|
||||||
|
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(", "),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue