Begin more work

This commit is contained in:
Jesse Wierzbinski 2023-09-19 14:16:50 -10:00
parent 1a1bee83a7
commit dacbc5a00b
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
6 changed files with 198 additions and 14 deletions

View file

@ -1,8 +1,9 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { APActor } from "activitypub-types";
import { getConfig } from "@config";
import { APActor, APOrderedCollectionPage, IconField } from "activitypub-types";
import { getConfig, getHost } from "@config";
import { appendFile } from "fs/promises";
import { errorResponse } from "@response";
import { APIAccount } from "~types/entities/account";
/**
* Stores an ActivityPub actor as raw JSON-LD data
@ -17,7 +18,10 @@ export class RawActor extends BaseEntity {
@Column("jsonb")
data!: APActor;
static async getById(id: string) {
@Column("jsonb")
followers!: string[];
static async getByActorId(id: string) {
return await RawActor.createQueryBuilder("actor")
.where("actor.data->>'id' = :id", {
id,
@ -59,6 +63,98 @@ export class RawActor extends BaseEntity {
return errorResponse("Actor already exists", 409);
}
getInstanceDomain() {
return new URL(this.data.id ?? "").host;
}
async fetchFollowers() {
// Fetch follower list using ActivityPub
// Loop to fetch all followers until there are no more pages
let followers: APOrderedCollectionPage = await fetch(
`${this.data.followers?.toString() ?? ""}?page=1`,
{
headers: {
Accept: "application/activity+json",
},
}
);
let followersList = followers.orderedItems ?? [];
while (followers.type === "OrderedCollectionPage" && followers.next) {
// Fetch next page
// eslint-disable-next-line @typescript-eslint/no-base-to-string
followers = await fetch(followers.next.toString(), {
headers: {
Accept: "application/activity+json",
},
}).then(res => res.json());
// Add new followers to list
followersList = {
...followersList,
...(followers.orderedItems ?? []),
};
}
this.followers = followersList as string[];
}
// eslint-disable-next-line @typescript-eslint/require-await
async toAPIAccount(isOwnAccount = false): Promise<APIAccount> {
const config = getConfig();
return {
id: this.id,
username: this.data.preferredUsername ?? "",
display_name: this.data.name ?? this.data.preferredUsername ?? "",
note: this.data.summary ?? "",
url: `${config.http.base_url}:${config.http.port}/@${
this.data.preferredUsername
}@${this.getInstanceDomain()}`,
// @ts-expect-error It actually works
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
avatar: (this.data.icon as IconField).url ?? config.defaults.avatar,
// @ts-expect-error It actually works
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
header: this.data.image?.url ?? config.defaults.header,
locked: false,
created_at: new Date(this.data.published ?? 0).toISOString(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
followers_count: 0,
following_count: 0,
statuses_count: 0,
emojis: [],
fields: [],
bot: false,
source: isOwnAccount
? {
privacy: "public",
sensitive: false,
language: "en",
note: "",
fields: [],
}
: undefined,
avatar_static: "",
header_static: "",
acct:
this.getInstanceDomain() == getHost()
? `${this.data.preferredUsername}`
: `${
this.data.preferredUsername
}@${this.getInstanceDomain()}`,
limited: false,
moved: null,
noindex: false,
suspended: false,
discoverable: undefined,
mute_expires_at: undefined,
group: false,
role: undefined,
};
}
async isObjectFiltered() {
const config = getConfig();
@ -109,6 +205,6 @@ export class RawActor extends BaseEntity {
}
static async exists(id: string) {
return !!(await RawActor.getById(id));
return !!(await RawActor.getByActorId(id));
}
}

View file

@ -1,7 +1,11 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { APObject } from "activitypub-types";
import { APActor, APObject, DateTime } from "activitypub-types";
import { getConfig } from "@config";
import { appendFile } from "fs/promises";
import { APIStatus } from "~types/entities/status";
import { RawActor } from "./RawActor";
import { APIAccount } from "~types/entities/account";
import { User } from "./User";
/**
* Stores an ActivityPub object as raw JSON-LD data
@ -24,6 +28,50 @@ export class RawObject extends BaseEntity {
.getOne();
}
async isPinned() {
}
async toAPI(): Promise<APIStatus> {
return {
account:
(await (
await RawActor.getByActorId(
(this.data.attributedTo as APActor).id ?? ""
)
)?.toAPIAccount()) ?? (null as unknown as APIAccount),
created_at: new Date(this.data.published as DateTime).toISOString(),
id: this.id,
in_reply_to_id: null,
application: null,
card: null,
content: this.data.content as string,
emojis: [],
favourited: false,
favourites_count: 0,
media_attachments: [],
mentions: [],
in_reply_to_account_id: null,
language: null,
muted: false,
pinned: false,
poll: null,
reblog: null,
reblogged: false,
reblogs_count: 0,
replies_count: 0,
sensitive: false,
spoiler_text: "",
tags: [],
uri: this.data.id as string,
visibility: "public",
url: this.data.id as string,
bookmarked: false,
quote: null,
quote_id: undefined,
};
}
async isObjectFiltered() {
const config = getConfig();

View file

@ -13,6 +13,7 @@ import {
import { APIAccount } from "~types/entities/account";
import { RawActor } from "./RawActor";
import { APActor } from "activitypub-types";
import { RawObject } from "./RawObject";
const config = getConfig();
@ -79,11 +80,20 @@ export class User extends BaseEntity {
@JoinTable()
following!: RawActor[];
@ManyToMany(() => RawActor, actor => actor.id)
@JoinTable()
followers!: RawActor[];
@ManyToMany(() => RawObject, object => object.id)
@JoinTable()
pinned_notes!: RawObject[];
static async getByActorId(id: string) {
return await User.createQueryBuilder("user")
// Objects is a many-to-many relationship
.leftJoinAndSelect("user.actor", "actor")
.leftJoinAndSelect("user.following", "following")
.leftJoinAndSelect("user.followers", "followers")
.where("actor.data @> :data", {
data: JSON.stringify({
id,

View file

@ -144,6 +144,29 @@ export default async (
}
break;
}
case "Follow" as APFollow: {
// Body is an APFollow object
// Add the actor to the object actor's followers list
const user = await User.getByActorId(
(body.actor as APActor).id ?? ""
);
if (!user) {
return errorResponse("User not found", 404);
}
const actor = await RawActor.addIfNotExists(body.actor as APActor);
if (actor instanceof Response) {
return actor;
}
user.followers.push(actor);
await user.save();
break;
}
}
return jsonResponse({});

View file

@ -1,6 +1,7 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { User } from "~database/entities/User";
import { RawActor } from "~database/entities/RawActor";
/**
* Fetch a user
@ -11,11 +12,17 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
const user = await User.findOneBy({
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
const user = await getUserByToken(token);
const foundUser = await RawActor.findOneBy({
id,
});
if (!user) return errorResponse("User not found", 404);
if (!foundUser) return errorResponse("User not found", 404);
return jsonResponse(user.toAPI());
return jsonResponse(
await foundUser.toAPIAccount(user?.id === foundUser.id)
);
};

View file

@ -147,11 +147,11 @@ export default async (req: Request): Promise<Response> => {
// TODO: Check if locale is valid
const newUser = new User();
newUser.username = body.username ?? "";
newUser.email = body.email ?? "";
newUser.password = await Bun.password.hash(body.password ?? "");
await User.createNew({
username: body.username ?? "",
password: body.password ?? "",
email: body.email ?? "",
});
// TODO: Return access token
return new Response();