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 { 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue