Refactor configs and activitypub parts

This commit is contained in:
Jesse Wierzbinski 2023-10-15 20:04:03 -10:00
parent ca7d325cb1
commit c0ff46559b
17 changed files with 251 additions and 70 deletions

18
benchmarks/fetch.ts Normal file
View 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`);

View file

@ -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();

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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;

View file

@ -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`
} }
] ]
}) })

View file

@ -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({

View file

@ -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;
} }

View file

@ -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,

View file

@ -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)

View file

@ -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 ?? ""
); );

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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,
}, },