Fix existing bugs in tests, refactor users

This commit is contained in:
Jesse Wierzbinski 2023-10-08 10:20:42 -10:00
parent b7587f8d3f
commit 65ff53e90c
19 changed files with 385 additions and 117 deletions

View file

@ -34,7 +34,7 @@ export class Emoji extends BaseEntity {
@ManyToOne(() => Instance, {
nullable: true,
})
instance!: Instance;
instance!: Instance | null;
/**
* The URL for the emoji.
@ -56,9 +56,9 @@ export class Emoji extends BaseEntity {
async toAPI(): Promise<APIEmoji> {
return {
shortcode: this.shortcode,
static_url: "",
url: "",
visible_in_picker: false,
static_url: this.url, // TODO: Add static version
url: this.url,
visible_in_picker: this.visible_in_picker,
category: undefined,
};
}

View file

@ -2,6 +2,7 @@ import {
BaseEntity,
Column,
Entity,
Index,
JoinTable,
ManyToMany,
PrimaryGeneratedColumn,
@ -23,6 +24,8 @@ export class RawActivity extends BaseEntity {
id!: string;
@Column("jsonb")
// Index ID for faster lookups
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
data!: APActivity;
@ManyToMany(() => RawObject)
@ -239,7 +242,6 @@ export class RawActivity extends BaseEntity {
const rawActor = new RawActor();
rawActor.data = actor;
rawActor.followers = [];
const config = getConfig();

View file

@ -3,18 +3,15 @@ import {
BaseEntity,
Column,
Entity,
JoinTable,
ManyToMany,
Index,
PrimaryGeneratedColumn,
} from "typeorm";
import { APActor, APImage, APOrderedCollectionPage } from "activitypub-types";
import { APActor, APImage } from "activitypub-types";
import { getConfig, getHost } from "@config";
import { appendFile } from "fs/promises";
import { errorResponse } from "@response";
import { APIAccount } from "~types/entities/account";
import { RawActivity } from "./RawActivity";
import { RawObject } from "./RawObject";
/**
* Represents a raw actor entity in the database.
*/
@ -30,21 +27,9 @@ export class RawActor extends BaseEntity {
* The ActivityPub actor data associated with the actor.
*/
@Column("jsonb")
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
data!: APActor;
/**
* The list of follower IDs associated with the actor.
*/
@Column("jsonb", { default: [] })
followers!: string[];
/**
* The list of featured objects associated with the actor.
*/
@ManyToMany(() => RawObject, { nullable: true })
@JoinTable()
featured!: RawObject[];
/**
* Retrieves a RawActor entity by actor ID.
* @param id The ID of the actor to retrieve.
@ -62,13 +47,13 @@ export class RawActor extends BaseEntity {
* @returns The newly created RawActor entity, or an error response if the actor already exists or is filtered.
*/
static async addIfNotExists(data: APActor) {
// TODO: Also add corresponding user
if (await RawActor.exists(data.id ?? "")) {
return errorResponse("Actor already exists", 409);
}
const actor = new RawActor();
actor.data = data;
actor.followers = [];
const config = getConfig();
@ -105,33 +90,6 @@ export class RawActor extends BaseEntity {
return new URL(this.data.id ?? "").host;
}
/**
* Fetches the list of followers associated with the actor and updates the `followers` property.
*/
async fetchFollowers() {
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) {
followers = await fetch((followers.next as string).toString(), {
headers: { Accept: "application/activity+json" },
}).then(res => res.json());
followersList = {
...followersList,
...(followers.orderedItems ?? []),
};
}
this.followers = followersList as string[];
}
/**
* Converts the RawActor entity to an API account object.
* @param isOwnAccount Whether the account is the user's own account.
@ -169,7 +127,7 @@ export class RawActor extends BaseEntity {
config.defaults.header,
locked: false,
created_at: new Date(published ?? 0).toISOString(),
followers_count: this.followers.length,
followers_count: 0,
following_count: 0,
statuses_count: statusCount,
emojis: [],
@ -249,7 +207,6 @@ export class RawActor extends BaseEntity {
* @returns Whether an actor with the specified ID exists in the database.
*/
static async exists(id: string) {
console.log(!!(await RawActor.getByActorId(id)));
return !!(await RawActor.getByActorId(id));
}
}

View file

@ -1,4 +1,10 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import {
BaseEntity,
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from "typeorm";
import { APImage, APObject, DateTime } from "activitypub-types";
import { getConfig } from "@config";
import { appendFile } from "fs/promises";
@ -6,6 +12,7 @@ import { APIStatus } from "~types/entities/status";
import { RawActor } from "./RawActor";
import { APIAccount } from "~types/entities/account";
import { APIEmoji } from "~types/entities/emoji";
import { User } from "./User";
/**
* Represents a raw ActivityPub object in the database.
@ -24,6 +31,11 @@ export class RawObject extends BaseEntity {
* The data associated with the object.
*/
@Column("jsonb")
// Index ID, attributedTo, to and published for faster lookups
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
@Index({ where: "(data->>'attributedTo') IS NOT NULL" })
@Index({ where: "(data->>'to') IS NOT NULL" })
@Index({ where: "(data->>'published') IS NOT NULL" })
data!: APObject;
/**
@ -82,10 +94,8 @@ export class RawObject extends BaseEntity {
return {
account:
(await (
await RawActor.getByActorId(
this.data.attributedTo as string
)
)?.toAPIAccount()) ?? (null as unknown as APIAccount),
await User.getByActorId(this.data.attributedTo as string)
)?.toAPI()) ?? (null as unknown as APIAccount),
created_at: new Date(this.data.published as DateTime).toISOString(),
id: this.id,
in_reply_to_id: null,

View file

@ -17,7 +17,6 @@ import { Application } from "./Application";
import { Emoji } from "./Emoji";
import { RawActivity } from "./RawActivity";
import { RawObject } from "./RawObject";
import { RawActor } from "./RawActor";
const config = getConfig();
@ -100,10 +99,10 @@ export class Status extends BaseEntity {
/**
* The raw actor that this status is a reply to, if any.
*/
@ManyToOne(() => RawActor, {
@ManyToOne(() => User, {
nullable: true,
})
in_reply_to_account!: RawActor | null;
in_reply_to_account!: User | null;
/**
* Whether this status is sensitive.
@ -196,7 +195,7 @@ export class Status extends BaseEntity {
emojis: Emoji[];
reply?: {
object: RawObject;
actor: RawActor;
user: User;
};
}) {
const newStatus = new Status();
@ -217,7 +216,7 @@ export class Status extends BaseEntity {
if (data.reply) {
newStatus.in_reply_to_post = data.reply.object;
newStatus.in_reply_to_account = data.reply.actor;
newStatus.in_reply_to_account = data.reply.user;
}
newStatus.object.data = {
@ -240,8 +239,8 @@ export class Status extends BaseEntity {
return `${config.http.base_url}/@${match[1]}`;
});
// Map this to Actors
const mentionedActors = (
// Map this to Users
const mentionedUsers = (
await Promise.all(
mentionedPeople.map(async person => {
// Check if post is in format @username or @username@instance.com
@ -252,23 +251,23 @@ export class Status extends BaseEntity {
: null;
if (instanceUrl) {
const actor = await RawActor.createQueryBuilder("actor")
.where("actor.data->>'id' = :id", {
// Where ID contains the instance URL
id: `%${instanceUrl}%`,
})
// Where actor preferredUsername is the username
.andWhere(
"actor.data->>'preferredUsername' = :username",
{
username: person.split("@")[1],
}
)
.getOne();
const user = await User.findOne({
where: {
username: person.split("@")[1],
// If contains instanceUrl
instance: {
base_url: instanceUrl,
},
},
relations: {
actor: true,
instance: true,
},
});
return actor?.data.id;
return user?.actor.data.id;
} else {
const actor = await User.findOne({
const user = await User.findOne({
where: {
username: person.split("@")[1],
},
@ -277,13 +276,13 @@ export class Status extends BaseEntity {
},
});
return actor?.actor.data.id;
return user?.actor.data.id;
}
})
)
).map(actor => actor as string);
).map(user => user as string);
newStatus.object.data.to = mentionedActors;
newStatus.object.data.to = mentionedUsers;
if (data.visibility === "private") {
newStatus.object.data.cc = [

View file

@ -14,12 +14,13 @@ import {
} from "typeorm";
import { APIAccount } from "~types/entities/account";
import { RawActor } from "./RawActor";
import { APActor } from "activitypub-types";
import { APActor, APOrderedCollectionPage } from "activitypub-types";
import { RawObject } from "./RawObject";
import { Token } from "./Token";
import { Status } from "./Status";
import { APISource } from "~types/entities/source";
import { Relationship } from "./Relationship";
import { Instance } from "./Instance";
/**
* Represents a user in the database.
@ -52,16 +53,19 @@ export class User extends BaseEntity {
/**
* The password for the user.
*/
@Column("varchar")
password!: string;
@Column("varchar", {
nullable: true,
})
password!: string | null;
/**
* The email address for the user.
*/
@Column("varchar", {
unique: true,
nullable: true,
})
email!: string;
email!: string | null;
/**
* The note for the user.
@ -118,8 +122,10 @@ export class User extends BaseEntity {
/**
* The private key for the user.
*/
@Column("varchar")
private_key!: string;
@Column("varchar", {
nullable: true,
})
private_key!: string | null;
/**
* The relationships for the user.
@ -127,6 +133,16 @@ export class User extends BaseEntity {
@OneToMany(() => Relationship, relationship => relationship.owner)
relationships!: Relationship[];
/**
* User's instance, null if local user
*/
@ManyToOne(() => Instance, {
nullable: true,
})
instance!: Instance | null;
/** */
/**
* The actor for the user.
*/
@ -140,6 +156,50 @@ export class User extends BaseEntity {
@JoinTable()
pinned_notes!: RawObject[];
/**
* Update this user data from its actor
* @returns The updated user.
*/
async updateFromActor() {
const actor = await this.actor.toAPIAccount();
this.username = actor.username;
this.display_name = actor.display_name;
this.note = actor.note;
this.avatar = actor.avatar;
this.header = actor.header;
this.avatar = actor.avatar;
return await this.save();
}
/**
* Fetches the list of followers associated with the actor and updates the user's followers
*/
async fetchFollowers() {
let followers: APOrderedCollectionPage = await fetch(
`${this.actor.data.followers?.toString() ?? ""}?page=1`,
{
headers: { Accept: "application/activity+json" },
}
);
let followersList = followers.orderedItems ?? [];
while (followers.type === "OrderedCollectionPage" && followers.next) {
followers = await fetch((followers.next as string).toString(), {
headers: { Accept: "application/activity+json" },
}).then(res => res.json());
followersList = {
...followersList,
...(followers.orderedItems ?? []),
};
}
// TODO: integrate followers
}
/**
* Gets a user by actor ID.
* @param id The actor ID to search for.
@ -159,11 +219,11 @@ export class User extends BaseEntity {
}
/**
* Creates a new user.
* Creates a new LOCAL user.
* @param data The data for the new user.
* @returns The newly created user.
*/
static async createNew(data: {
static async createNewLocal(data: {
username: string;
display_name?: string;
password: string;
@ -184,6 +244,7 @@ export class User extends BaseEntity {
user.header = data.header ?? config.defaults.avatar;
user.relationships = [];
user.instance = null;
user.source = {
language: null,
@ -401,7 +462,32 @@ export class User extends BaseEntity {
}
// eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIAccount> {
return await this.actor.toAPIAccount();
async toAPI(isOwnAccount = false): Promise<APIAccount> {
const follower_count = await Relationship.count({
where: {
subject: {
id: this.id,
},
following: true,
},
relations: ["subject"],
});
const following_count = await Relationship.count({
where: {
owner: {
id: this.id,
},
following: true,
},
relations: ["owner"],
});
return {
...(await this.actor.toAPIAccount(isOwnAccount)),
id: this.id,
followers_count: follower_count,
following_count: following_count,
};
}
}