mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
More Lysand protocol work, refactor keys, small refactoring overall
This commit is contained in:
parent
77a675afe6
commit
a1c0164e9d
11 changed files with 304 additions and 89 deletions
|
|
@ -39,15 +39,15 @@ export class LysandObject extends BaseEntity {
|
|||
uri!: string;
|
||||
|
||||
@Column("timestamp")
|
||||
created_at!: string;
|
||||
created_at!: Date;
|
||||
|
||||
/**
|
||||
* References an Actor object by URI
|
||||
* References an Actor object
|
||||
*/
|
||||
@ManyToOne(() => LysandObject, object => object.uri, {
|
||||
nullable: true,
|
||||
})
|
||||
author!: LysandObject;
|
||||
author!: LysandObject | null;
|
||||
|
||||
@Column("jsonb")
|
||||
extra_data!: Omit<
|
||||
|
|
@ -62,10 +62,65 @@ export class LysandObject extends BaseEntity {
|
|||
const object = new LysandObject();
|
||||
object.type = type;
|
||||
object.uri = uri;
|
||||
object.created_at = new Date().toISOString();
|
||||
object.created_at = new Date();
|
||||
return object;
|
||||
}
|
||||
|
||||
static async createFromObject(object: LysandObjectType) {
|
||||
let newObject: LysandObject;
|
||||
|
||||
const foundObject = await LysandObject.findOne({
|
||||
where: { remote_id: object.id },
|
||||
relations: ["author"],
|
||||
});
|
||||
|
||||
if (foundObject) {
|
||||
newObject = foundObject;
|
||||
} else {
|
||||
newObject = new LysandObject();
|
||||
}
|
||||
|
||||
const author = await LysandObject.findOne({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
where: { uri: (object as any).author },
|
||||
});
|
||||
|
||||
newObject.author = author;
|
||||
newObject.created_at = new Date(object.created_at);
|
||||
newObject.extensions = object.extensions || {};
|
||||
newObject.remote_id = object.id;
|
||||
newObject.type = object.type;
|
||||
newObject.uri = object.uri;
|
||||
// Rest of data (remove id, author, created_at, extensions, type, uri)
|
||||
newObject.extra_data = Object.fromEntries(
|
||||
Object.entries(object).filter(
|
||||
([key]) =>
|
||||
![
|
||||
"id",
|
||||
"author",
|
||||
"created_at",
|
||||
"extensions",
|
||||
"type",
|
||||
"uri",
|
||||
].includes(key)
|
||||
)
|
||||
);
|
||||
|
||||
await newObject.save();
|
||||
return newObject;
|
||||
}
|
||||
|
||||
toLysand(): LysandObjectType {
|
||||
return {
|
||||
id: this.remote_id || this.id,
|
||||
created_at: new Date(this.created_at).toISOString(),
|
||||
type: this.type,
|
||||
uri: this.uri,
|
||||
...this.extra_data,
|
||||
extensions: this.extensions,
|
||||
};
|
||||
}
|
||||
|
||||
isPublication(): boolean {
|
||||
return this.type === "Note" || this.type === "Patch";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ import { Emoji } from "./Emoji";
|
|||
import { Instance } from "./Instance";
|
||||
import { Like } from "./Like";
|
||||
import { AppDataSource } from "~database/datasource";
|
||||
import { Note } from "~types/lysand/Object";
|
||||
import { LysandPublication, Note } from "~types/lysand/Object";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { getBestContentType } from "@content_types";
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
|
@ -39,7 +40,13 @@ export const statusRelations = [
|
|||
|
||||
export const statusAndUserRelations = [
|
||||
...statusRelations,
|
||||
...["account.relationships", "account.pinned_notes", "account.instance"],
|
||||
// Can't directly map to userRelations as variable isnt yet initialized
|
||||
...[
|
||||
"account.relationships",
|
||||
"account.pinned_notes",
|
||||
"account.instance",
|
||||
"account.emojis",
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -56,6 +63,12 @@ export class Status extends BaseEntity {
|
|||
@PrimaryGeneratedColumn("uuid")
|
||||
id!: string;
|
||||
|
||||
/**
|
||||
* The URI for this status.
|
||||
*/
|
||||
@Column("varchar")
|
||||
uri!: string;
|
||||
|
||||
/**
|
||||
* The user account that created this status.
|
||||
*/
|
||||
|
|
@ -119,6 +132,14 @@ export class Status extends BaseEntity {
|
|||
})
|
||||
in_reply_to_post!: Status | null;
|
||||
|
||||
/**
|
||||
* The status that this status is quoting, if any
|
||||
*/
|
||||
@ManyToOne(() => Status, status => status.id, {
|
||||
nullable: true,
|
||||
})
|
||||
quoting_post!: Status | null;
|
||||
|
||||
@TreeChildren()
|
||||
replies!: Status[];
|
||||
|
||||
|
|
@ -223,6 +244,64 @@ export class Status extends BaseEntity {
|
|||
}
|
||||
}
|
||||
|
||||
static async fetchFromRemote(uri: string) {
|
||||
// Check if already in database
|
||||
|
||||
const existingStatus = await Status.findOne({
|
||||
where: {
|
||||
uri: uri,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingStatus) return existingStatus;
|
||||
|
||||
const status = await fetch(uri);
|
||||
|
||||
if (status.status === 404) return null;
|
||||
|
||||
const body = (await status.json()) as LysandPublication;
|
||||
|
||||
const content = getBestContentType(body.contents);
|
||||
|
||||
const emojis = await Emoji.parseEmojis(content?.content || "");
|
||||
|
||||
const author = await User.fetchRemoteUser(body.author);
|
||||
|
||||
let replyStatus: Status | null = null;
|
||||
let quotingStatus: Status | null = null;
|
||||
|
||||
if (body.replies_to.length > 0) {
|
||||
replyStatus = await Status.fetchFromRemote(body.replies_to[0]);
|
||||
}
|
||||
|
||||
if (body.quotes.length > 0) {
|
||||
quotingStatus = await Status.fetchFromRemote(body.quotes[0]);
|
||||
}
|
||||
|
||||
const newStatus = await Status.createNew({
|
||||
account: author,
|
||||
content: content?.content || "",
|
||||
content_type: content?.content_type,
|
||||
application: null,
|
||||
// TODO: Add visibility
|
||||
visibility: "public",
|
||||
spoiler_text: body.subject || "",
|
||||
uri: body.uri,
|
||||
sensitive: body.is_sensitive,
|
||||
emojis: emojis,
|
||||
mentions: await User.parseMentions(body.mentions),
|
||||
reply: replyStatus
|
||||
? {
|
||||
status: replyStatus,
|
||||
user: replyStatus.account,
|
||||
}
|
||||
: undefined,
|
||||
quote: quotingStatus || undefined,
|
||||
});
|
||||
|
||||
return await newStatus.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the ancestors of this post,
|
||||
*/
|
||||
|
|
@ -300,11 +379,13 @@ export class Status extends BaseEntity {
|
|||
spoiler_text: string;
|
||||
emojis: Emoji[];
|
||||
content_type?: string;
|
||||
uri?: string;
|
||||
mentions?: User[];
|
||||
reply?: {
|
||||
status: Status;
|
||||
user: User;
|
||||
};
|
||||
quote?: Status;
|
||||
}) {
|
||||
const newStatus = new Status();
|
||||
|
||||
|
|
@ -319,6 +400,9 @@ export class Status extends BaseEntity {
|
|||
newStatus.isReblog = false;
|
||||
newStatus.mentions = [];
|
||||
newStatus.instance = data.account.instance;
|
||||
newStatus.uri =
|
||||
data.uri || `${config.http.base_url}/statuses/${newStatus.id}`;
|
||||
newStatus.quoting_post = data.quote || null;
|
||||
|
||||
if (data.reply) {
|
||||
newStatus.in_reply_to_post = data.reply.status;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ import { User as LysandUser } from "~types/lysand/Object";
|
|||
import { htmlToText } from "html-to-text";
|
||||
import { Emoji } from "./Emoji";
|
||||
|
||||
export const userRelations = ["relationships", "pinned_notes", "instance"];
|
||||
export const userRelations = [
|
||||
"relationships",
|
||||
"pinned_notes",
|
||||
"instance",
|
||||
"emojis",
|
||||
];
|
||||
|
||||
/**
|
||||
* Represents a user in the database.
|
||||
|
|
@ -210,6 +215,15 @@ export class User extends BaseEntity {
|
|||
}
|
||||
|
||||
static async fetchRemoteUser(uri: string) {
|
||||
// Check if user not already in database
|
||||
const foundUser = await User.findOne({
|
||||
where: {
|
||||
uri,
|
||||
},
|
||||
});
|
||||
|
||||
if (foundUser) return foundUser;
|
||||
|
||||
const response = await fetch(uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
|
|
@ -329,6 +343,7 @@ export class User extends BaseEntity {
|
|||
user.avatar = data.avatar ?? config.defaults.avatar;
|
||||
user.header = data.header ?? config.defaults.avatar;
|
||||
user.uri = `${config.http.base_url}/users/${user.id}`;
|
||||
user.emojis = [];
|
||||
|
||||
user.relationships = [];
|
||||
user.instance = null;
|
||||
|
|
@ -349,6 +364,22 @@ export class User extends BaseEntity {
|
|||
return user;
|
||||
}
|
||||
|
||||
static async parseMentions(mentions: string[]) {
|
||||
return await Promise.all(
|
||||
mentions.map(async mention => {
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
uri: mention,
|
||||
},
|
||||
relations: userRelations,
|
||||
});
|
||||
|
||||
if (user) return user;
|
||||
else return await User.fetchRemoteUser(mention);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a user from a token.
|
||||
* @param access_token The access token to retrieve the user from.
|
||||
|
|
@ -448,20 +479,16 @@ export class User extends BaseEntity {
|
|||
* Generates keys for the user.
|
||||
*/
|
||||
async generateKeys(): Promise<void> {
|
||||
const keys = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: "ed25519",
|
||||
namedCurve: "ed25519",
|
||||
},
|
||||
true,
|
||||
["sign", "verify"]
|
||||
);
|
||||
const keys = (await crypto.subtle.generateKey("Ed25519", true, [
|
||||
"sign",
|
||||
"verify",
|
||||
])) as CryptoKeyPair;
|
||||
|
||||
const privateKey = btoa(
|
||||
String.fromCharCode.apply(null, [
|
||||
...new Uint8Array(
|
||||
// jesus help me what do these letters mean
|
||||
await crypto.subtle.exportKey("raw", keys.privateKey)
|
||||
await crypto.subtle.exportKey("pkcs8", keys.privateKey)
|
||||
),
|
||||
])
|
||||
);
|
||||
|
|
@ -469,13 +496,13 @@ export class User extends BaseEntity {
|
|||
String.fromCharCode(
|
||||
...new Uint8Array(
|
||||
// why is exporting a key so hard
|
||||
await crypto.subtle.exportKey("raw", keys.publicKey)
|
||||
await crypto.subtle.exportKey("spki", keys.publicKey)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Add header, footer and newlines later on
|
||||
// These keys are PEM encrypted
|
||||
// These keys are base64 encrypted
|
||||
this.private_key = privateKey;
|
||||
this.public_key = publicKey;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue