mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Refactor configs and activitypub parts
This commit is contained in:
parent
ca7d325cb1
commit
c0ff46559b
18
benchmarks/fetch.ts
Normal file
18
benchmarks/fetch.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
const timeBefore = performance.now();
|
||||||
|
|
||||||
|
const requests: Promise<Response>[] = [];
|
||||||
|
|
||||||
|
// Repeat 1000 times
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
requests.push(
|
||||||
|
fetch(`https://mastodon.social`, {
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(requests);
|
||||||
|
|
||||||
|
const timeAfter = performance.now();
|
||||||
|
|
||||||
|
console.log(`Time taken: ${timeAfter - timeBefore}ms`);
|
||||||
|
|
@ -72,7 +72,7 @@ const timeAfter = performance.now();
|
||||||
|
|
||||||
const activities = await RawActivity.createQueryBuilder("activity")
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
.where("activity.data->>'actor' = :actor", {
|
.where("activity.data->>'actor' = :actor", {
|
||||||
actor: `${config.http.base_url}/@test`,
|
actor: `${config.http.base_url}/users/test`,
|
||||||
})
|
})
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,25 @@ bind_port = "8080"
|
||||||
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
|
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
|
||||||
banned_ips = []
|
banned_ips = []
|
||||||
|
|
||||||
|
[smtp]
|
||||||
|
# SMTP server to use for sending emails
|
||||||
|
server = "smtp.example.com"
|
||||||
|
port = 465
|
||||||
|
username = "test@example.com"
|
||||||
|
password = "password123"
|
||||||
|
tls = true
|
||||||
|
|
||||||
|
[email]
|
||||||
|
# Sends an email to moderators when a report is received
|
||||||
|
# NOT IMPLEMENTED
|
||||||
|
send_on_report = false
|
||||||
|
# Sends an email to moderators when a user is suspended
|
||||||
|
# NOT IMPLEMENTED
|
||||||
|
send_on_suspend = false
|
||||||
|
# Sends an email to moderators when a user is unsuspended
|
||||||
|
# NOT IMPLEMENTED
|
||||||
|
send_on_unsuspend = false
|
||||||
|
|
||||||
[validation]
|
[validation]
|
||||||
# Self explanatory
|
# Self explanatory
|
||||||
max_displayname_size = 50
|
max_displayname_size = 50
|
||||||
|
|
@ -20,9 +39,9 @@ max_bio_size = 160
|
||||||
max_note_size = 5000
|
max_note_size = 5000
|
||||||
max_avatar_size = 5_000_000
|
max_avatar_size = 5_000_000
|
||||||
max_header_size = 5_000_000
|
max_header_size = 5_000_000
|
||||||
max_media_size = 40_000_000
|
max_media_size = 40_000_000 # MEDIA NOT IMPLEMENTED
|
||||||
max_media_attachments = 4
|
max_media_attachments = 4 # MEDIA NOT IMPLEMENTED
|
||||||
max_media_description_size = 1000
|
max_media_description_size = 1000 # MEDIA NOT IMPLEMENTED
|
||||||
max_username_size = 30
|
max_username_size = 30
|
||||||
# An array of strings, defaults are from Akkoma
|
# An array of strings, defaults are from Akkoma
|
||||||
username_blacklist = [ ".well-known", "~", "about", "activities" , "api",
|
username_blacklist = [ ".well-known", "~", "about", "activities" , "api",
|
||||||
|
|
@ -36,12 +55,12 @@ email_blacklist = []
|
||||||
# Valid URL schemes, otherwise the URL is parsed as text
|
# Valid URL schemes, otherwise the URL is parsed as text
|
||||||
url_scheme_whitelist = [ "http", "https", "ftp", "dat", "dweb", "gopher", "hyper",
|
url_scheme_whitelist = [ "http", "https", "ftp", "dat", "dweb", "gopher", "hyper",
|
||||||
"ipfs", "ipns", "irc", "xmpp", "ircs", "magnet", "mailto", "mumble", "ssb",
|
"ipfs", "ipns", "irc", "xmpp", "ircs", "magnet", "mailto", "mumble", "ssb",
|
||||||
"gemini" ]
|
"gemini" ] # NOT IMPLEMENTED
|
||||||
allowed_mime_types = [ "image/jpeg", "image/png", "image/gif", "image/heic", "image/heif",
|
allowed_mime_types = [ "image/jpeg", "image/png", "image/gif", "image/heic", "image/heif",
|
||||||
"image/webp", "image/avif", "video/webm", "video/mp4", "video/quicktime", "video/ogg",
|
"image/webp", "image/avif", "video/webm", "video/mp4", "video/quicktime", "video/ogg",
|
||||||
"audio/wave", "audio/wav", "audio/x-wav", "audio/x-pn-wave", "audio/vnd.wave",
|
"audio/wave", "audio/wav", "audio/x-wav", "audio/x-pn-wave", "audio/vnd.wave",
|
||||||
"audio/ogg", "audio/vorbis", "audio/mpeg", "audio/mp3", "audio/webm", "audio/flac",
|
"audio/ogg", "audio/vorbis", "audio/mpeg", "audio/mp3", "audio/webm", "audio/flac",
|
||||||
"audio/aac", "audio/m4a", "audio/x-m4a", "audio/mp4", "audio/3gpp", "video/x-ms-asf" ]
|
"audio/aac", "audio/m4a", "audio/x-m4a", "audio/mp4", "audio/3gpp", "video/x-ms-asf" ] # MEDIA NOT IMPLEMENTED
|
||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
# Default visibility for new notes
|
# Default visibility for new notes
|
||||||
|
|
@ -58,25 +77,29 @@ header = ""
|
||||||
use_tombstones = true
|
use_tombstones = true
|
||||||
# Fetch all members of collections (followers, following, etc) when receiving them
|
# Fetch all members of collections (followers, following, etc) when receiving them
|
||||||
# WARNING: This can be a lot of data, and is not recommended
|
# WARNING: This can be a lot of data, and is not recommended
|
||||||
fetch_all_collection_members = false
|
fetch_all_collection_members = false # NOT IMPLEMENTED
|
||||||
|
|
||||||
# The following values must be instance domain names without "https" or glob patterns
|
# The following values must be instance domain names without "https" or glob patterns
|
||||||
# Rejects all activities from these instances, simply doesnt save them at all
|
# Rejects all activities from these instances (fediblocking)
|
||||||
reject_activities = []
|
reject_activities = []
|
||||||
# Force posts from this instance to be followers only
|
# Force posts from this instance to be followers only
|
||||||
force_followers_only = []
|
force_followers_only = [] # NOT IMPLEMENTED
|
||||||
# Discard all reports from these instances
|
# Discard all reports from these instances
|
||||||
discard_reports = []
|
discard_reports = [] # NOT IMPLEMENTED
|
||||||
# Discard all deletes from these instances
|
# Discard all deletes from these instances
|
||||||
discard_deletes = []
|
discard_deletes = []
|
||||||
|
# Discard all updates (edits) from these instances
|
||||||
|
discard_updates = []
|
||||||
# Discard all banners from these instances
|
# Discard all banners from these instances
|
||||||
discard_banners = []
|
discard_banners = [] # NOT IMPLEMENTED
|
||||||
# Discard all avatars from these instances
|
# Discard all avatars from these instances
|
||||||
discard_avatars = []
|
discard_avatars = [] # NOT IMPLEMENTED
|
||||||
|
# Discard all follow requests from these instances
|
||||||
|
discard_follows = []
|
||||||
# Force set these instances' media as sensitive
|
# Force set these instances' media as sensitive
|
||||||
force_sensitive = []
|
force_sensitive = [] # NOT IMPLEMENTED
|
||||||
# Remove theses instances' media
|
# Remove theses instances' media
|
||||||
remove_media = []
|
remove_media = [] # NOT IMPLEMENTED
|
||||||
|
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
|
|
@ -91,7 +114,7 @@ username_filters = []
|
||||||
displayname_filters = []
|
displayname_filters = []
|
||||||
# Drop users with these regex filters (only applies to new activities)
|
# Drop users with these regex filters (only applies to new activities)
|
||||||
bio_filters = []
|
bio_filters = []
|
||||||
emoji_filters = []
|
emoji_filters = [] # NOT IMPLEMENTED
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
# Log all requests (warning: this is a lot of data)
|
# Log all requests (warning: this is a lot of data)
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export class RawActor extends BaseEntity {
|
||||||
username: preferredUsername ?? "",
|
username: preferredUsername ?? "",
|
||||||
display_name: name ?? preferredUsername ?? "",
|
display_name: name ?? preferredUsername ?? "",
|
||||||
note: summary ?? "",
|
note: summary ?? "",
|
||||||
url: `${config.http.base_url}/@${preferredUsername}${
|
url: `${config.http.base_url}/users/${preferredUsername}${
|
||||||
isLocalUser ? "" : `@${this.getInstanceDomain()}`
|
isLocalUser ? "" : `@${this.getInstanceDomain()}`
|
||||||
}`,
|
}`,
|
||||||
avatar:
|
avatar:
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ export class Status extends BaseEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
newStatus.object.data = {
|
newStatus.object.data = {
|
||||||
id: `${config.http.base_url}/@${data.account.username}/statuses/${newStatus.id}`,
|
id: `${config.http.base_url}/users/${data.account.username}/statuses/${newStatus.id}`,
|
||||||
type: "Note",
|
type: "Note",
|
||||||
summary: data.spoiler_text,
|
summary: data.spoiler_text,
|
||||||
content: data.content, // TODO: Format as HTML
|
content: data.content, // TODO: Format as HTML
|
||||||
|
|
@ -229,14 +229,14 @@ export class Status extends BaseEntity {
|
||||||
: undefined,
|
: undefined,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
tag: [],
|
tag: [],
|
||||||
attributedTo: `${config.http.base_url}/@${data.account.username}`,
|
attributedTo: `${config.http.base_url}/users/${data.account.username}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get people mentioned in the content
|
// Get people mentioned in the content
|
||||||
const mentionedPeople = [
|
const mentionedPeople = [
|
||||||
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g),
|
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g),
|
||||||
].map(match => {
|
].map(match => {
|
||||||
return `${config.http.base_url}/@${match[1]}`;
|
return `${config.http.base_url}/users/${match[1]}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map this to Users
|
// Map this to Users
|
||||||
|
|
@ -286,7 +286,7 @@ export class Status extends BaseEntity {
|
||||||
|
|
||||||
if (data.visibility === "private") {
|
if (data.visibility === "private") {
|
||||||
newStatus.object.data.cc = [
|
newStatus.object.data.cc = [
|
||||||
`${config.http.base_url}/@${data.account.username}/followers`,
|
`${config.http.base_url}/users/${data.account.username}/followers`,
|
||||||
];
|
];
|
||||||
} else if (data.visibility === "direct") {
|
} else if (data.visibility === "direct") {
|
||||||
// Add nothing else
|
// Add nothing else
|
||||||
|
|
@ -296,7 +296,7 @@ export class Status extends BaseEntity {
|
||||||
"https://www.w3.org/ns/activitystreams#Public",
|
"https://www.w3.org/ns/activitystreams#Public",
|
||||||
];
|
];
|
||||||
newStatus.object.data.cc = [
|
newStatus.object.data.cc = [
|
||||||
`${config.http.base_url}/@${data.account.username}/followers`,
|
`${config.http.base_url}/users/${data.account.username}/followers`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
|
|
||||||
|
|
@ -379,7 +379,7 @@ export class User extends BaseEntity {
|
||||||
|
|
||||||
// Check if actor exists
|
// Check if actor exists
|
||||||
const actorExists = await RawActor.getByActorId(
|
const actorExists = await RawActor.getByActorId(
|
||||||
`${config.http.base_url}/@${this.username}`
|
`${config.http.base_url}/users/${this.username}`
|
||||||
);
|
);
|
||||||
|
|
||||||
let actor: RawActor;
|
let actor: RawActor;
|
||||||
|
|
@ -395,14 +395,14 @@ export class User extends BaseEntity {
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"https://w3id.org/security/v1",
|
"https://w3id.org/security/v1",
|
||||||
],
|
],
|
||||||
id: `${config.http.base_url}/@${this.username}`,
|
id: `${config.http.base_url}/users/${this.username}`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: this.username,
|
preferredUsername: this.username,
|
||||||
name: this.display_name,
|
name: this.display_name,
|
||||||
inbox: `${config.http.base_url}/@${this.username}/inbox`,
|
inbox: `${config.http.base_url}/users/${this.username}/inbox`,
|
||||||
outbox: `${config.http.base_url}/@${this.username}/outbox`,
|
outbox: `${config.http.base_url}/users/${this.username}/outbox`,
|
||||||
followers: `${config.http.base_url}/@${this.username}/followers`,
|
followers: `${config.http.base_url}/users/${this.username}/followers`,
|
||||||
following: `${config.http.base_url}/@${this.username}/following`,
|
following: `${config.http.base_url}/users/${this.username}/following`,
|
||||||
manuallyApprovesFollowers: false,
|
manuallyApprovesFollowers: false,
|
||||||
summary: this.note,
|
summary: this.note,
|
||||||
icon: {
|
icon: {
|
||||||
|
|
@ -414,8 +414,8 @@ export class User extends BaseEntity {
|
||||||
url: this.header,
|
url: this.header,
|
||||||
},
|
},
|
||||||
publicKey: {
|
publicKey: {
|
||||||
id: `${config.http.base_url}/@${this.username}/actor#main-key`,
|
id: `${config.http.base_url}/users/${this.username}/actor#main-key`,
|
||||||
owner: `${config.http.base_url}/@${this.username}/actor`,
|
owner: `${config.http.base_url}/users/${this.username}/actor`,
|
||||||
publicKeyPem: this.public_key,
|
publicKeyPem: this.public_key,
|
||||||
},
|
},
|
||||||
} as APActor;
|
} as APActor;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { MatchedRoute } from "bun";
|
import { MatchedRoute } from "bun";
|
||||||
import { User } from "~database/entities/User";
|
import { User } from "~database/entities/User";
|
||||||
import { getHost } from "@config";
|
import { getConfig, getHost } from "@config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub WebFinger endpoint
|
* ActivityPub WebFinger endpoint
|
||||||
|
|
@ -14,6 +14,8 @@ export default async (
|
||||||
const resource = matchedRoute.query.resource;
|
const resource = matchedRoute.query.resource;
|
||||||
const requestedUser = resource.split("acct:")[1];
|
const requestedUser = resource.split("acct:")[1];
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
// Check if user is a local user
|
// Check if user is a local user
|
||||||
if (requestedUser.split("@")[1] !== getHost()) {
|
if (requestedUser.split("@")[1] !== getHost()) {
|
||||||
return errorResponse("User is a remote user", 404);
|
return errorResponse("User is a remote user", 404);
|
||||||
|
|
@ -32,17 +34,17 @@ export default async (
|
||||||
{
|
{
|
||||||
rel: "self",
|
rel: "self",
|
||||||
type: "application/activity+json",
|
type: "application/activity+json",
|
||||||
href: `https://${getHost()}/@${user.username}/actor`
|
href: `${config.http.base_url}/users/${user.username}/actor`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "https://webfinger.net/rel/profile-page",
|
rel: "https://webfinger.net/rel/profile-page",
|
||||||
type: "text/html",
|
type: "text/html",
|
||||||
href: `https://${getHost()}/@${user.username}`
|
href: `${config.http.base_url}/users/${user.username}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "self",
|
rel: "self",
|
||||||
type: "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
type: "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||||
href: `https://${getHost()}/@${user.username}/actor`
|
href: `${config.http.base_url}/users/${user.username}/actor`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,6 @@ export default async (req: Request): Promise<Response> => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
config.validation.max_username_size;
|
|
||||||
|
|
||||||
// Check if username is valid
|
// Check if username is valid
|
||||||
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
|
|
@ -83,6 +81,18 @@ export default async (req: Request): Promise<Response> => {
|
||||||
description: `must only contain letters, numbers, and underscores`,
|
description: `must only contain letters, numbers, and underscores`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if username doesnt match filters
|
||||||
|
if (
|
||||||
|
config.filters.username_filters.some(
|
||||||
|
filter => body.username?.match(filter)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
errors.details.username.push({
|
||||||
|
error: "ERR_INVALID",
|
||||||
|
description: `contains blocked words`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if username is too long
|
// Check if username is too long
|
||||||
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,15 @@ export default async (req: Request): Promise<Response> => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if display name doesnt match filters
|
||||||
|
if (
|
||||||
|
config.filters.displayname_filters.some(filter =>
|
||||||
|
display_name.match(filter)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return errorResponse("Display name contains blocked words", 422);
|
||||||
|
}
|
||||||
|
|
||||||
user.actor.data.name = display_name;
|
user.actor.data.name = display_name;
|
||||||
user.display_name = display_name;
|
user.display_name = display_name;
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +84,11 @@ export default async (req: Request): Promise<Response> => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if bio doesnt match filters
|
||||||
|
if (config.filters.bio_filters.some(filter => note.match(filter))) {
|
||||||
|
return errorResponse("Bio contains blocked words", 422);
|
||||||
|
}
|
||||||
|
|
||||||
user.actor.data.summary = note;
|
user.actor.data.summary = note;
|
||||||
user.note = note;
|
user.note = note;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,11 @@ export default async (req: Request): Promise<Response> => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if status body doesnt match filters
|
||||||
|
if (config.filters.note_filters.some(filter => status.match(filter))) {
|
||||||
|
return errorResponse("Status contains blocked words", 422);
|
||||||
|
}
|
||||||
|
|
||||||
// Create status
|
// Create status
|
||||||
const newStatus = await Status.createNew({
|
const newStatus = await Status.createNew({
|
||||||
account: user,
|
account: user,
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ export default async (
|
||||||
Hashtag: "as:Hashtag",
|
Hashtag: "as:Hashtag",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
id: `${config.http.base_url}/@${user.username}`,
|
id: `${config.http.base_url}/users/${user.username}`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: user.username, // TODO: Add user display name
|
preferredUsername: user.username, // TODO: Add user display name
|
||||||
name: user.username,
|
name: user.username,
|
||||||
|
|
@ -104,21 +104,21 @@ export default async (
|
||||||
url: user.header,
|
url: user.header,
|
||||||
mediaType: "image/png", // TODO: Set user header mimetype
|
mediaType: "image/png", // TODO: Set user header mimetype
|
||||||
},
|
},
|
||||||
inbox: `${config.http.base_url}/@${user.username}/inbox`,
|
inbox: `${config.http.base_url}/users/${user.username}/inbox`,
|
||||||
outbox: `${config.http.base_url}/@${user.username}/outbox`,
|
outbox: `${config.http.base_url}/users/${user.username}/outbox`,
|
||||||
followers: `${config.http.base_url}/@${user.username}/followers`,
|
followers: `${config.http.base_url}/users/${user.username}/followers`,
|
||||||
following: `${config.http.base_url}/@${user.username}/following`,
|
following: `${config.http.base_url}/users/${user.username}/following`,
|
||||||
liked: `${config.http.base_url}/@${user.username}/liked`,
|
liked: `${config.http.base_url}/users/${user.username}/liked`,
|
||||||
discoverable: true,
|
discoverable: true,
|
||||||
alsoKnownAs: [
|
alsoKnownAs: [
|
||||||
// TODO: Add accounts from which the user migrated
|
// TODO: Add accounts from which the user migrated
|
||||||
],
|
],
|
||||||
manuallyApprovesFollowers: false, // TODO: Change
|
manuallyApprovesFollowers: false, // TODO: Change
|
||||||
publicKey: {
|
publicKey: {
|
||||||
id: `${getHost()}${config.http.base_url}/@${
|
id: `${getHost()}${config.http.base_url}/users/${
|
||||||
user.username
|
user.username
|
||||||
}/actor#main-key`,
|
}/actor#main-key`,
|
||||||
owner: `${config.http.base_url}/@${user.username}`,
|
owner: `${config.http.base_url}/users/${user.username}`,
|
||||||
// Split the public key into PEM format
|
// Split the public key into PEM format
|
||||||
publicKeyPem: `-----BEGIN PUBLIC KEY-----\n${user.public_key
|
publicKeyPem: `-----BEGIN PUBLIC KEY-----\n${user.public_key
|
||||||
.match(/.{1,64}/g)
|
.match(/.{1,64}/g)
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
|
@ -30,11 +31,61 @@ export default async (
|
||||||
return errorResponse("Method not allowed", 405);
|
return errorResponse("Method not allowed", 405);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const username = matchedRoute.params.username;
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
config.activitypub.reject_activities.includes(
|
||||||
|
new URL(req.headers.get("Origin") ?? "").hostname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Discard request
|
||||||
|
return jsonResponse({});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`[-] Error parsing Origin header of incoming Activity from ${req.headers.get(
|
||||||
|
"Origin"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
// Process request body
|
// Process request body
|
||||||
const body: APActivity = await req.json();
|
const body: APActivity = await req.json();
|
||||||
|
|
||||||
|
// Verify HTTP signature
|
||||||
|
const signature = req.headers.get("Signature") ?? "";
|
||||||
|
const signatureParams = signature
|
||||||
|
.split(",")
|
||||||
|
.reduce<Record<string, string>>((params, param) => {
|
||||||
|
const [key, value] = param.split("=");
|
||||||
|
params[key] = value.replace(/"/g, "");
|
||||||
|
return params;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const signedString = `(request-target): post /users/${username}/inbox\nhost: ${
|
||||||
|
config.http.base_url
|
||||||
|
}\ndate: ${req.headers.get("Date")}`;
|
||||||
|
const signatureBuffer = new TextEncoder().encode(signatureParams.signature);
|
||||||
|
const signatureBytes = new Uint8Array(signatureBuffer).buffer;
|
||||||
|
const publicKeyBuffer = (body.actor as any).publicKey.publicKeyPem;
|
||||||
|
const publicKey = await crypto.subtle.importKey(
|
||||||
|
"spki",
|
||||||
|
publicKeyBuffer,
|
||||||
|
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||||
|
false,
|
||||||
|
["verify"]
|
||||||
|
);
|
||||||
|
const verified = await crypto.subtle.verify(
|
||||||
|
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
||||||
|
publicKey,
|
||||||
|
signatureBytes,
|
||||||
|
new TextEncoder().encode(signedString)
|
||||||
|
);
|
||||||
|
|
||||||
// Get the object's ActivityPub type
|
// Get the object's ActivityPub type
|
||||||
const type = body.type;
|
const type = body.type;
|
||||||
|
|
||||||
|
|
@ -57,6 +108,24 @@ export default async (
|
||||||
// Replace the object in database with the new provided object
|
// Replace the object in database with the new provided object
|
||||||
// TODO: Add authentication
|
// TODO: Add authentication
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
config.activitypub.discard_updates.includes(
|
||||||
|
new URL(req.headers.get("Origin") ?? "").hostname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Discard request
|
||||||
|
return jsonResponse({});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`[-] Error parsing Origin header of incoming Update Activity from ${req.headers.get(
|
||||||
|
"Origin"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
const object = await RawActivity.updateObjectIfExists(
|
const object = await RawActivity.updateObjectIfExists(
|
||||||
body.object as APObject
|
body.object as APObject
|
||||||
);
|
);
|
||||||
|
|
@ -78,6 +147,24 @@ export default async (
|
||||||
// Delete the object from database
|
// Delete the object from database
|
||||||
// TODO: Add authentication
|
// TODO: Add authentication
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
config.activitypub.discard_deletes.includes(
|
||||||
|
new URL(req.headers.get("Origin") ?? "").hostname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Discard request
|
||||||
|
return jsonResponse({});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`[-] Error parsing Origin header of incoming Delete Activity from ${req.headers.get(
|
||||||
|
"Origin"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await RawActivity.deleteObjectIfExists(
|
const response = await RawActivity.deleteObjectIfExists(
|
||||||
body.object as APObject
|
body.object as APObject
|
||||||
);
|
);
|
||||||
|
|
@ -152,6 +239,24 @@ export default async (
|
||||||
// Body is an APFollow object
|
// Body is an APFollow object
|
||||||
// Add the actor to the object actor's followers list
|
// Add the actor to the object actor's followers list
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
config.activitypub.discard_follows.includes(
|
||||||
|
new URL(req.headers.get("Origin") ?? "").hostname
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Reject request
|
||||||
|
return jsonResponse({});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`[-] Error parsing Origin header of incoming Delete Activity from ${req.headers.get(
|
||||||
|
"Origin"
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.getByActorId(
|
const user = await User.getByActorId(
|
||||||
(body.actor as APActor).id ?? ""
|
(body.actor as APActor).id ?? ""
|
||||||
);
|
);
|
||||||
|
|
@ -23,7 +23,7 @@ beforeAll(async () => {
|
||||||
|
|
||||||
describe("POST /@test/actor", () => {
|
describe("POST /@test/actor", () => {
|
||||||
test("should return a valid ActivityPub Actor when querying an existing user", async () => {
|
test("should return a valid ActivityPub Actor when querying an existing user", async () => {
|
||||||
const response = await fetch(`${config.http.base_url}/@test/actor`, {
|
const response = await fetch(`${config.http.base_url}/users/test/actor`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/activity+json",
|
Accept: "application/activity+json",
|
||||||
|
|
@ -38,16 +38,16 @@ describe("POST /@test/actor", () => {
|
||||||
const actor: APActor = await response.json();
|
const actor: APActor = await response.json();
|
||||||
|
|
||||||
expect(actor.type).toBe("Person");
|
expect(actor.type).toBe("Person");
|
||||||
expect(actor.id).toBe(`${config.http.base_url}/@test`);
|
expect(actor.id).toBe(`${config.http.base_url}/users/test`);
|
||||||
expect(actor.preferredUsername).toBe("test");
|
expect(actor.preferredUsername).toBe("test");
|
||||||
expect(actor.inbox).toBe(`${config.http.base_url}/@test/inbox`);
|
expect(actor.inbox).toBe(`${config.http.base_url}/users/test/inbox`);
|
||||||
expect(actor.outbox).toBe(`${config.http.base_url}/@test/outbox`);
|
expect(actor.outbox).toBe(`${config.http.base_url}/users/test/outbox`);
|
||||||
expect(actor.followers).toBe(`${config.http.base_url}/@test/followers`);
|
expect(actor.followers).toBe(`${config.http.base_url}/users/test/followers`);
|
||||||
expect(actor.following).toBe(`${config.http.base_url}/@test/following`);
|
expect(actor.following).toBe(`${config.http.base_url}/users/test/following`);
|
||||||
expect((actor as any).publicKey).toBeDefined();
|
expect((actor as any).publicKey).toBeDefined();
|
||||||
expect((actor as any).publicKey.id).toBeDefined();
|
expect((actor as any).publicKey.id).toBeDefined();
|
||||||
expect((actor as any).publicKey.owner).toBe(
|
expect((actor as any).publicKey.owner).toBe(
|
||||||
`${config.http.base_url}/@test`
|
`${config.http.base_url}/users/test`
|
||||||
);
|
);
|
||||||
expect((actor as any).publicKey.publicKeyPem).toBeDefined();
|
expect((actor as any).publicKey.publicKeyPem).toBeDefined();
|
||||||
expect((actor as any).publicKey.publicKeyPem).toMatch(
|
expect((actor as any).publicKey.publicKeyPem).toMatch(
|
||||||
|
|
@ -64,7 +64,7 @@ afterAll(async () => {
|
||||||
|
|
||||||
const activities = await RawActivity.createQueryBuilder("activity")
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
.where("activity.data->>'actor' = :actor", {
|
.where("activity.data->>'actor' = :actor", {
|
||||||
actor: `${config.http.base_url}/@test`,
|
actor: `${config.http.base_url}/users/test`,
|
||||||
})
|
})
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ describe("GET /api/v1/accounts/verify_credentials", () => {
|
||||||
expect(account.following_count).toBe(0);
|
expect(account.following_count).toBe(0);
|
||||||
expect(account.statuses_count).toBe(0);
|
expect(account.statuses_count).toBe(0);
|
||||||
expect(account.note).toBe("");
|
expect(account.note).toBe("");
|
||||||
expect(account.url).toBe(`${config.http.base_url}/@${user.username}`);
|
expect(account.url).toBe(`${config.http.base_url}/users/${user.username}`);
|
||||||
expect(account.avatar).toBeDefined();
|
expect(account.avatar).toBeDefined();
|
||||||
expect(account.avatar_static).toBeDefined();
|
expect(account.avatar_static).toBeDefined();
|
||||||
expect(account.header).toBeDefined();
|
expect(account.header).toBeDefined();
|
||||||
|
|
@ -719,7 +719,7 @@ describe("GET /api/v1/custom_emojis", () => {
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
const activities = await RawActivity.createQueryBuilder("activity")
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
.where("activity.data->>'actor' = :actor", {
|
.where("activity.data->>'actor' = :actor", {
|
||||||
actor: `${config.http.base_url}/@test`,
|
actor: `${config.http.base_url}/users/test`,
|
||||||
})
|
})
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ describe("POST /@test/inbox", () => {
|
||||||
test("should store a new Note object", async () => {
|
test("should store a new Note object", async () => {
|
||||||
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
|
|
||||||
const response = await fetch(`${config.http.base_url}/@test/inbox/`, {
|
const response = await fetch(`${config.http.base_url}/users/test/inbox/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/activity+json",
|
"Content-Type": "application/activity+json",
|
||||||
|
|
@ -33,7 +33,7 @@ describe("POST /@test/inbox", () => {
|
||||||
type: "Create",
|
type: "Create",
|
||||||
id: activityId,
|
id: activityId,
|
||||||
actor: {
|
actor: {
|
||||||
id: `${config.http.base_url}/@test`,
|
id: `${config.http.base_url}/users/test`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: "test",
|
preferredUsername: "test",
|
||||||
},
|
},
|
||||||
|
|
@ -82,7 +82,7 @@ describe("POST /@test/inbox", () => {
|
||||||
test("should try to update that Note object", async () => {
|
test("should try to update that Note object", async () => {
|
||||||
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
|
|
||||||
const response = await fetch(`${config.http.base_url}/@test/inbox/`, {
|
const response = await fetch(`${config.http.base_url}/users/test/inbox/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/activity+json",
|
"Content-Type": "application/activity+json",
|
||||||
|
|
@ -92,7 +92,7 @@ describe("POST /@test/inbox", () => {
|
||||||
type: "Update",
|
type: "Update",
|
||||||
id: activityId,
|
id: activityId,
|
||||||
actor: {
|
actor: {
|
||||||
id: `${config.http.base_url}/@test`,
|
id: `${config.http.base_url}/users/test`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: "test",
|
preferredUsername: "test",
|
||||||
},
|
},
|
||||||
|
|
@ -140,7 +140,7 @@ describe("POST /@test/inbox", () => {
|
||||||
|
|
||||||
test("should delete the Note object", async () => {
|
test("should delete the Note object", async () => {
|
||||||
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
const response = await fetch(`${config.http.base_url}/@test/inbox/`, {
|
const response = await fetch(`${config.http.base_url}/users/test/inbox/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/activity+json",
|
"Content-Type": "application/activity+json",
|
||||||
|
|
@ -150,7 +150,7 @@ describe("POST /@test/inbox", () => {
|
||||||
type: "Delete",
|
type: "Delete",
|
||||||
id: activityId,
|
id: activityId,
|
||||||
actor: {
|
actor: {
|
||||||
id: `${config.http.base_url}/@test`,
|
id: `${config.http.base_url}/users/test`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: "test",
|
preferredUsername: "test",
|
||||||
},
|
},
|
||||||
|
|
@ -187,17 +187,17 @@ describe("POST /@test/inbox", () => {
|
||||||
expect(activity?.actors).toHaveLength(1);
|
expect(activity?.actors).toHaveLength(1);
|
||||||
expect(activity?.actors[0].data).toEqual({
|
expect(activity?.actors[0].data).toEqual({
|
||||||
preferredUsername: "test",
|
preferredUsername: "test",
|
||||||
id: `${config.http.base_url}/@test`,
|
id: `${config.http.base_url}/users/test`,
|
||||||
summary: "",
|
summary: "",
|
||||||
publicKey: {
|
publicKey: {
|
||||||
id: `${config.http.base_url}/@test/actor#main-key`,
|
id: `${config.http.base_url}/users/test/actor#main-key`,
|
||||||
owner: `${config.http.base_url}/@test/actor`,
|
owner: `${config.http.base_url}/users/test/actor`,
|
||||||
publicKeyPem: expect.any(String),
|
publicKeyPem: expect.any(String),
|
||||||
},
|
},
|
||||||
outbox: `${config.http.base_url}/@test/outbox`,
|
outbox: `${config.http.base_url}/users/test/outbox`,
|
||||||
manuallyApprovesFollowers: false,
|
manuallyApprovesFollowers: false,
|
||||||
followers: `${config.http.base_url}/@test/followers`,
|
followers: `${config.http.base_url}/users/test/followers`,
|
||||||
following: `${config.http.base_url}/@test/following`,
|
following: `${config.http.base_url}/users/test/following`,
|
||||||
name: "",
|
name: "",
|
||||||
"@context": [
|
"@context": [
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
|
@ -211,7 +211,7 @@ describe("POST /@test/inbox", () => {
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: "",
|
url: "",
|
||||||
},
|
},
|
||||||
inbox: `${config.http.base_url}/@test/inbox`,
|
inbox: `${config.http.base_url}/users/test/inbox`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -226,7 +226,7 @@ describe("POST /@test/inbox", () => {
|
||||||
test("should return a 404 error when trying to delete a non-existent Note object", async () => {
|
test("should return a 404 error when trying to delete a non-existent Note object", async () => {
|
||||||
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
const activityId = `https://example.com/objects/${crypto.randomUUID()}`;
|
||||||
|
|
||||||
const response = await fetch(`${config.http.base_url}/@test/inbox/`, {
|
const response = await fetch(`${config.http.base_url}/users/test/inbox/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/activity+json",
|
"Content-Type": "application/activity+json",
|
||||||
|
|
@ -236,7 +236,7 @@ describe("POST /@test/inbox", () => {
|
||||||
type: "Delete",
|
type: "Delete",
|
||||||
id: activityId,
|
id: activityId,
|
||||||
actor: {
|
actor: {
|
||||||
id: `${config.http.base_url}/@test`,
|
id: `${config.http.base_url}/users/test`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: "test",
|
preferredUsername: "test",
|
||||||
},
|
},
|
||||||
|
|
@ -274,10 +274,10 @@ afterAll(async () => {
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
.leftJoinAndSelect("activity.actors", "actors")
|
.leftJoinAndSelect("activity.actors", "actors")
|
||||||
// activity.actors is a many-to-many relationship with Actor objects (it is an array of Actor objects)
|
// activity.actors is a many-to-many relationship with Actor objects (it is an array of Actor objects)
|
||||||
// Get the actors of the activity that have data.id as `${config.http.base_url}/@test`
|
// Get the actors of the activity that have data.id as `${config.http.base_url}/users/test`
|
||||||
.where("actors.data @> :data", {
|
.where("actors.data @> :data", {
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
id: `${config.http.base_url}/@test`,
|
id: `${config.http.base_url}/users/test`,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ export interface ConfigType {
|
||||||
discard_deletes: string[];
|
discard_deletes: string[];
|
||||||
discard_banners: string[];
|
discard_banners: string[];
|
||||||
discard_avatars: string[];
|
discard_avatars: string[];
|
||||||
|
discard_updates: string[];
|
||||||
|
discard_follows: string[];
|
||||||
force_sensitive: string[];
|
force_sensitive: string[];
|
||||||
remove_media: string[];
|
remove_media: string[];
|
||||||
fetch_all_colletion_members: boolean;
|
fetch_all_colletion_members: boolean;
|
||||||
|
|
@ -178,6 +180,8 @@ export const configDefaults: ConfigType = {
|
||||||
discard_banners: [],
|
discard_banners: [],
|
||||||
discard_avatars: [],
|
discard_avatars: [],
|
||||||
force_sensitive: [],
|
force_sensitive: [],
|
||||||
|
discard_updates: [],
|
||||||
|
discard_follows: [],
|
||||||
remove_media: [],
|
remove_media: [],
|
||||||
fetch_all_colletion_members: false,
|
fetch_all_colletion_members: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue