mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Begin more work
This commit is contained in:
parent
1a1bee83a7
commit
dacbc5a00b
|
|
@ -1,8 +1,9 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { APActor } from "activitypub-types";
|
import { APActor, APOrderedCollectionPage, IconField } from "activitypub-types";
|
||||||
import { getConfig } from "@config";
|
import { getConfig, getHost } from "@config";
|
||||||
import { appendFile } from "fs/promises";
|
import { appendFile } from "fs/promises";
|
||||||
import { errorResponse } from "@response";
|
import { errorResponse } from "@response";
|
||||||
|
import { APIAccount } from "~types/entities/account";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an ActivityPub actor as raw JSON-LD data
|
* Stores an ActivityPub actor as raw JSON-LD data
|
||||||
|
|
@ -17,7 +18,10 @@ export class RawActor extends BaseEntity {
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
data!: APActor;
|
data!: APActor;
|
||||||
|
|
||||||
static async getById(id: string) {
|
@Column("jsonb")
|
||||||
|
followers!: string[];
|
||||||
|
|
||||||
|
static async getByActorId(id: string) {
|
||||||
return await RawActor.createQueryBuilder("actor")
|
return await RawActor.createQueryBuilder("actor")
|
||||||
.where("actor.data->>'id' = :id", {
|
.where("actor.data->>'id' = :id", {
|
||||||
id,
|
id,
|
||||||
|
|
@ -59,6 +63,98 @@ export class RawActor extends BaseEntity {
|
||||||
return errorResponse("Actor already exists", 409);
|
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() {
|
async isObjectFiltered() {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -109,6 +205,6 @@ export class RawActor extends BaseEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async exists(id: string) {
|
static async exists(id: string) {
|
||||||
return !!(await RawActor.getById(id));
|
return !!(await RawActor.getByActorId(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { APObject } from "activitypub-types";
|
import { APActor, APObject, DateTime } from "activitypub-types";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { appendFile } from "fs/promises";
|
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
|
* Stores an ActivityPub object as raw JSON-LD data
|
||||||
|
|
@ -24,6 +28,50 @@ export class RawObject extends BaseEntity {
|
||||||
.getOne();
|
.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() {
|
async isObjectFiltered() {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
import { RawActor } from "./RawActor";
|
import { RawActor } from "./RawActor";
|
||||||
import { APActor } from "activitypub-types";
|
import { APActor } from "activitypub-types";
|
||||||
|
import { RawObject } from "./RawObject";
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -79,11 +80,20 @@ export class User extends BaseEntity {
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
following!: RawActor[];
|
following!: RawActor[];
|
||||||
|
|
||||||
|
@ManyToMany(() => RawActor, actor => actor.id)
|
||||||
|
@JoinTable()
|
||||||
|
followers!: RawActor[];
|
||||||
|
|
||||||
|
@ManyToMany(() => RawObject, object => object.id)
|
||||||
|
@JoinTable()
|
||||||
|
pinned_notes!: RawObject[];
|
||||||
|
|
||||||
static async getByActorId(id: string) {
|
static async getByActorId(id: string) {
|
||||||
return await User.createQueryBuilder("user")
|
return await User.createQueryBuilder("user")
|
||||||
// Objects is a many-to-many relationship
|
// Objects is a many-to-many relationship
|
||||||
.leftJoinAndSelect("user.actor", "actor")
|
.leftJoinAndSelect("user.actor", "actor")
|
||||||
.leftJoinAndSelect("user.following", "following")
|
.leftJoinAndSelect("user.following", "following")
|
||||||
|
.leftJoinAndSelect("user.followers", "followers")
|
||||||
.where("actor.data @> :data", {
|
.where("actor.data @> :data", {
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,29 @@ export default async (
|
||||||
}
|
}
|
||||||
break;
|
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({});
|
return jsonResponse({});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { getUserByToken } from "@auth";
|
||||||
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 { RawActor } from "~database/entities/RawActor";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a user
|
* Fetch a user
|
||||||
|
|
@ -11,11 +12,17 @@ export default async (
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
const id = matchedRoute.params.id;
|
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,
|
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)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -147,11 +147,11 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
// TODO: Check if locale is valid
|
// TODO: Check if locale is valid
|
||||||
|
|
||||||
const newUser = new User();
|
await User.createNew({
|
||||||
|
username: body.username ?? "",
|
||||||
newUser.username = body.username ?? "";
|
password: body.password ?? "",
|
||||||
newUser.email = body.email ?? "";
|
email: body.email ?? "",
|
||||||
newUser.password = await Bun.password.hash(body.password ?? "");
|
});
|
||||||
|
|
||||||
// TODO: Return access token
|
// TODO: Return access token
|
||||||
return new Response();
|
return new Response();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue