mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
More Lysand protocol work, refactor keys, small refactoring overall
This commit is contained in:
parent
77a675afe6
commit
a1c0164e9d
|
|
@ -39,15 +39,15 @@ export class LysandObject extends BaseEntity {
|
||||||
uri!: string;
|
uri!: string;
|
||||||
|
|
||||||
@Column("timestamp")
|
@Column("timestamp")
|
||||||
created_at!: string;
|
created_at!: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* References an Actor object by URI
|
* References an Actor object
|
||||||
*/
|
*/
|
||||||
@ManyToOne(() => LysandObject, object => object.uri, {
|
@ManyToOne(() => LysandObject, object => object.uri, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
author!: LysandObject;
|
author!: LysandObject | null;
|
||||||
|
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
extra_data!: Omit<
|
extra_data!: Omit<
|
||||||
|
|
@ -62,10 +62,65 @@ export class LysandObject extends BaseEntity {
|
||||||
const object = new LysandObject();
|
const object = new LysandObject();
|
||||||
object.type = type;
|
object.type = type;
|
||||||
object.uri = uri;
|
object.uri = uri;
|
||||||
object.created_at = new Date().toISOString();
|
object.created_at = new Date();
|
||||||
return object;
|
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 {
|
isPublication(): boolean {
|
||||||
return this.type === "Note" || this.type === "Patch";
|
return this.type === "Note" || this.type === "Patch";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ import { Emoji } from "./Emoji";
|
||||||
import { Instance } from "./Instance";
|
import { Instance } from "./Instance";
|
||||||
import { Like } from "./Like";
|
import { Like } from "./Like";
|
||||||
import { AppDataSource } from "~database/datasource";
|
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 { htmlToText } from "html-to-text";
|
||||||
|
import { getBestContentType } from "@content_types";
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -39,7 +40,13 @@ export const statusRelations = [
|
||||||
|
|
||||||
export const statusAndUserRelations = [
|
export const statusAndUserRelations = [
|
||||||
...statusRelations,
|
...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")
|
@PrimaryGeneratedColumn("uuid")
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URI for this status.
|
||||||
|
*/
|
||||||
|
@Column("varchar")
|
||||||
|
uri!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user account that created this status.
|
* The user account that created this status.
|
||||||
*/
|
*/
|
||||||
|
|
@ -119,6 +132,14 @@ export class Status extends BaseEntity {
|
||||||
})
|
})
|
||||||
in_reply_to_post!: Status | null;
|
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()
|
@TreeChildren()
|
||||||
replies!: Status[];
|
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,
|
* Return all the ancestors of this post,
|
||||||
*/
|
*/
|
||||||
|
|
@ -300,11 +379,13 @@ export class Status extends BaseEntity {
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
emojis: Emoji[];
|
emojis: Emoji[];
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
|
uri?: string;
|
||||||
mentions?: User[];
|
mentions?: User[];
|
||||||
reply?: {
|
reply?: {
|
||||||
status: Status;
|
status: Status;
|
||||||
user: User;
|
user: User;
|
||||||
};
|
};
|
||||||
|
quote?: Status;
|
||||||
}) {
|
}) {
|
||||||
const newStatus = new Status();
|
const newStatus = new Status();
|
||||||
|
|
||||||
|
|
@ -319,6 +400,9 @@ export class Status extends BaseEntity {
|
||||||
newStatus.isReblog = false;
|
newStatus.isReblog = false;
|
||||||
newStatus.mentions = [];
|
newStatus.mentions = [];
|
||||||
newStatus.instance = data.account.instance;
|
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) {
|
if (data.reply) {
|
||||||
newStatus.in_reply_to_post = data.reply.status;
|
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 { htmlToText } from "html-to-text";
|
||||||
import { Emoji } from "./Emoji";
|
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.
|
* Represents a user in the database.
|
||||||
|
|
@ -210,6 +215,15 @@ export class User extends BaseEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchRemoteUser(uri: string) {
|
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, {
|
const response = await fetch(uri, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -329,6 +343,7 @@ export class User extends BaseEntity {
|
||||||
user.avatar = data.avatar ?? config.defaults.avatar;
|
user.avatar = data.avatar ?? config.defaults.avatar;
|
||||||
user.header = data.header ?? config.defaults.avatar;
|
user.header = data.header ?? config.defaults.avatar;
|
||||||
user.uri = `${config.http.base_url}/users/${user.id}`;
|
user.uri = `${config.http.base_url}/users/${user.id}`;
|
||||||
|
user.emojis = [];
|
||||||
|
|
||||||
user.relationships = [];
|
user.relationships = [];
|
||||||
user.instance = null;
|
user.instance = null;
|
||||||
|
|
@ -349,6 +364,22 @@ export class User extends BaseEntity {
|
||||||
return user;
|
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.
|
* Retrieves a user from a token.
|
||||||
* @param access_token The access token to retrieve the user from.
|
* @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.
|
* Generates keys for the user.
|
||||||
*/
|
*/
|
||||||
async generateKeys(): Promise<void> {
|
async generateKeys(): Promise<void> {
|
||||||
const keys = await crypto.subtle.generateKey(
|
const keys = (await crypto.subtle.generateKey("Ed25519", true, [
|
||||||
{
|
"sign",
|
||||||
name: "ed25519",
|
"verify",
|
||||||
namedCurve: "ed25519",
|
])) as CryptoKeyPair;
|
||||||
},
|
|
||||||
true,
|
|
||||||
["sign", "verify"]
|
|
||||||
);
|
|
||||||
|
|
||||||
const privateKey = btoa(
|
const privateKey = btoa(
|
||||||
String.fromCharCode.apply(null, [
|
String.fromCharCode.apply(null, [
|
||||||
...new Uint8Array(
|
...new Uint8Array(
|
||||||
// jesus help me what do these letters mean
|
// 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(
|
String.fromCharCode(
|
||||||
...new Uint8Array(
|
...new Uint8Array(
|
||||||
// why is exporting a key so hard
|
// 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
|
// Add header, footer and newlines later on
|
||||||
// These keys are PEM encrypted
|
// These keys are base64 encrypted
|
||||||
this.private_key = privateKey;
|
this.private_key = privateKey;
|
||||||
this.public_key = publicKey;
|
this.public_key = publicKey;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { applyConfig } from "@api";
|
import { applyConfig } from "@api";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
|
import { getBestContentType } from "@content_types";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { MatchedRoute } from "bun";
|
import { MatchedRoute } from "bun";
|
||||||
|
import { Emoji } from "~database/entities/Emoji";
|
||||||
|
import { LysandObject } from "~database/entities/Object";
|
||||||
import { Status } from "~database/entities/Status";
|
import { Status } from "~database/entities/Status";
|
||||||
import { User, userRelations } from "~database/entities/User";
|
import { User, userRelations } from "~database/entities/User";
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,6 +14,7 @@ import {
|
||||||
LysandAction,
|
LysandAction,
|
||||||
LysandObjectType,
|
LysandObjectType,
|
||||||
LysandPublication,
|
LysandPublication,
|
||||||
|
Patch,
|
||||||
} from "~types/lysand/Object";
|
} from "~types/lysand/Object";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
|
|
@ -111,28 +115,23 @@ export default async (
|
||||||
|
|
||||||
// author.public_key is base64 encoded raw public key
|
// author.public_key is base64 encoded raw public key
|
||||||
const publicKey = await crypto.subtle.importKey(
|
const publicKey = await crypto.subtle.importKey(
|
||||||
"raw",
|
"spki",
|
||||||
Buffer.from(author.public_key, "base64"),
|
Buffer.from(author.public_key, "base64"),
|
||||||
{
|
"Ed25519",
|
||||||
name: "ed25519",
|
|
||||||
},
|
|
||||||
false,
|
false,
|
||||||
["verify"]
|
["verify"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if signed string is valid
|
// Check if signed string is valid
|
||||||
const isValid = await crypto.subtle.verify(
|
const isValid = await crypto.subtle.verify(
|
||||||
{
|
"Ed25519",
|
||||||
name: "ed25519",
|
|
||||||
saltLength: 0,
|
|
||||||
},
|
|
||||||
publicKey,
|
publicKey,
|
||||||
new TextEncoder().encode(signature),
|
Buffer.from(signature, "base64"),
|
||||||
new TextEncoder().encode(expectedSignedString)
|
new TextEncoder().encode(expectedSignedString)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error("Invalid signature");
|
return errorResponse("Invalid signature", 401);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,42 +140,14 @@ export default async (
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "Note": {
|
case "Note": {
|
||||||
let content: ContentFormat | null;
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
|
|
||||||
// Find the best content and content type
|
const content = getBestContentType(body.contents);
|
||||||
if (
|
|
||||||
body.contents.find(
|
|
||||||
c => c.content_type === "text/x.misskeymarkdown"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
content =
|
|
||||||
body.contents.find(
|
|
||||||
c => c.content_type === "text/x.misskeymarkdown"
|
|
||||||
) || null;
|
|
||||||
} else if (
|
|
||||||
body.contents.find(c => c.content_type === "text/html")
|
|
||||||
) {
|
|
||||||
content =
|
|
||||||
body.contents.find(c => c.content_type === "text/html") ||
|
|
||||||
null;
|
|
||||||
} else if (
|
|
||||||
body.contents.find(c => c.content_type === "text/markdown")
|
|
||||||
) {
|
|
||||||
content =
|
|
||||||
body.contents.find(
|
|
||||||
c => c.content_type === "text/markdown"
|
|
||||||
) || null;
|
|
||||||
} else if (
|
|
||||||
body.contents.find(c => c.content_type === "text/plain")
|
|
||||||
) {
|
|
||||||
content =
|
|
||||||
body.contents.find(c => c.content_type === "text/plain") ||
|
|
||||||
null;
|
|
||||||
} else {
|
|
||||||
content = body.contents[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await Status.createNew({
|
const emojis = await Emoji.parseEmojis(content?.content || "");
|
||||||
|
|
||||||
|
const newStatus = await Status.createNew({
|
||||||
account: author,
|
account: author,
|
||||||
content: content?.content || "",
|
content: content?.content || "",
|
||||||
content_type: content?.content_type,
|
content_type: content?.content_type,
|
||||||
|
|
@ -185,34 +156,106 @@ export default async (
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
spoiler_text: body.subject || "",
|
spoiler_text: body.subject || "",
|
||||||
sensitive: body.is_sensitive,
|
sensitive: body.is_sensitive,
|
||||||
// TODO: Add emojis
|
uri: body.uri,
|
||||||
emojis: [],
|
emojis: emojis,
|
||||||
|
mentions: await User.parseMentions(body.mentions),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If there is a reply, fetch all the reply parents and add them to the database
|
||||||
|
if (body.replies_to.length > 0) {
|
||||||
|
newStatus.in_reply_to_post = await Status.fetchFromRemote(
|
||||||
|
body.replies_to[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same for quotes
|
||||||
|
if (body.quotes.length > 0) {
|
||||||
|
newStatus.quoting_post = await Status.fetchFromRemote(
|
||||||
|
body.quotes[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await newStatus.save();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Patch": {
|
case "Patch": {
|
||||||
|
const patch = body as Patch;
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(patch);
|
||||||
|
|
||||||
|
// Edit the status
|
||||||
|
|
||||||
|
const content = getBestContentType(patch.contents);
|
||||||
|
|
||||||
|
const emojis = await Emoji.parseEmojis(content?.content || "");
|
||||||
|
|
||||||
|
const status = await Status.findOneBy({
|
||||||
|
id: patch.patched_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return errorResponse("Status not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
status.content = content?.content || "";
|
||||||
|
status.content_type = content?.content_type || "text/plain";
|
||||||
|
status.spoiler_text = patch.subject || "";
|
||||||
|
status.sensitive = patch.is_sensitive;
|
||||||
|
status.emojis = emojis;
|
||||||
|
|
||||||
|
// If there is a reply, fetch all the reply parents and add them to the database
|
||||||
|
if (body.replies_to.length > 0) {
|
||||||
|
status.in_reply_to_post = await Status.fetchFromRemote(
|
||||||
|
body.replies_to[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same for quotes
|
||||||
|
if (body.quotes.length > 0) {
|
||||||
|
status.quoting_post = await Status.fetchFromRemote(
|
||||||
|
body.quotes[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Like": {
|
case "Like": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Dislike": {
|
case "Dislike": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Follow": {
|
case "Follow": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "FollowAccept": {
|
case "FollowAccept": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "FollowReject": {
|
case "FollowReject": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Announce": {
|
case "Announce": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Undo": {
|
case "Undo": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Extension": {
|
||||||
|
// Store the object in the LysandObject table
|
||||||
|
await LysandObject.createFromObject(body);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { jsonResponse } from "@response";
|
import { jsonResponse } from "@response";
|
||||||
import { MatchedRoute } from "bun";
|
import { MatchedRoute } from "bun";
|
||||||
import { userRelations } from "~database/entities/User";
|
import { userRelations } from "~database/entities/User";
|
||||||
import { getHost } from "@config";
|
import { getConfig, getHost } from "@config";
|
||||||
import { applyConfig } from "@api";
|
import { applyConfig } from "@api";
|
||||||
import { Status } from "~database/entities/Status";
|
import { Status } from "~database/entities/Status";
|
||||||
import { In } from "typeorm";
|
import { In } from "typeorm";
|
||||||
|
|
@ -27,6 +27,7 @@ export default async (
|
||||||
): Promise<Response> => {
|
): Promise<Response> => {
|
||||||
const uuid = matchedRoute.params.uuid;
|
const uuid = matchedRoute.params.uuid;
|
||||||
const pageNumber = Number(matchedRoute.query.page) || 1;
|
const pageNumber = Number(matchedRoute.query.page) || 1;
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
const statuses = await Status.find({
|
const statuses = await Status.find({
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -54,6 +55,8 @@ export default async (
|
||||||
first: `${getHost()}/users/${uuid}/outbox?page=1`,
|
first: `${getHost()}/users/${uuid}/outbox?page=1`,
|
||||||
last: `${getHost()}/users/${uuid}/outbox?page=1`,
|
last: `${getHost()}/users/${uuid}/outbox?page=1`,
|
||||||
total_items: totalStatuses,
|
total_items: totalStatuses,
|
||||||
|
// Server actor
|
||||||
|
author: `${config.http.base_url}/users/actor`,
|
||||||
next:
|
next:
|
||||||
statuses.length === 20
|
statuses.length === 20
|
||||||
? `${getHost()}/users/${uuid}/outbox?page=${pageNumber + 1}`
|
? `${getHost()}/users/${uuid}/outbox?page=${pageNumber + 1}`
|
||||||
|
|
@ -109,7 +109,8 @@ describe("API Tests", () => {
|
||||||
const emoji = new Emoji();
|
const emoji = new Emoji();
|
||||||
|
|
||||||
emoji.instance = null;
|
emoji.instance = null;
|
||||||
emoji.url = "https://example.com";
|
emoji.url = "https://example.com/test.png";
|
||||||
|
emoji.content_type = "image/png";
|
||||||
emoji.shortcode = "test";
|
emoji.shortcode = "test";
|
||||||
emoji.visible_in_picker = true;
|
emoji.visible_in_picker = true;
|
||||||
|
|
||||||
|
|
@ -135,7 +136,7 @@ describe("API Tests", () => {
|
||||||
|
|
||||||
expect(emojis.length).toBeGreaterThan(0);
|
expect(emojis.length).toBeGreaterThan(0);
|
||||||
expect(emojis[0].shortcode).toBe("test");
|
expect(emojis[0].shortcode).toBe("test");
|
||||||
expect(emojis[0].url).toBe("https://example.com");
|
expect(emojis[0].url).toBe("https://example.com/test.png");
|
||||||
});
|
});
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await Emoji.delete({ shortcode: "test" });
|
await Emoji.delete({ shortcode: "test" });
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,7 @@ describe("API Tests", () => {
|
||||||
const status1 = statuses[1];
|
const status1 = statuses[1];
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
expect(status1.content).toBe("Hello, world!");
|
expect(status1.content).toBe("This is a reply!");
|
||||||
expect(status1.visibility).toBe("public");
|
expect(status1.visibility).toBe("public");
|
||||||
expect(status1.account.id).toBe(user.id);
|
expect(status1.account.id).toBe(user.id);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
/* import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { AppDataSource } from "~database/datasource";
|
import { AppDataSource } from "~database/datasource";
|
||||||
import { Instance } from "~database/entities/Instance";
|
import { Instance } from "~database/entities/Instance";
|
||||||
|
|
||||||
|
|
@ -14,25 +14,6 @@ describe("Instance", () => {
|
||||||
instance = await Instance.addIfNotExists(url);
|
instance = await Instance.addIfNotExists(url);
|
||||||
expect(instance.base_url).toBe("mastodon.social");
|
expect(instance.base_url).toBe("mastodon.social");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert the instance to an API instance", async () => {
|
|
||||||
const apiInstance = await instance.toAPI();
|
|
||||||
expect(apiInstance.uri).toBe("mastodon.social");
|
|
||||||
expect(apiInstance.approval_required).toBe(false);
|
|
||||||
expect(apiInstance.email).toBe("staff@mastodon.social");
|
|
||||||
expect(apiInstance.thumbnail).toBeDefined();
|
|
||||||
expect(apiInstance.title).toBeDefined();
|
|
||||||
expect(apiInstance.configuration).toBeDefined();
|
|
||||||
expect(apiInstance.contact_account).toBeDefined();
|
|
||||||
expect(apiInstance.description).toBeDefined();
|
|
||||||
expect(apiInstance.invites_enabled).toBeDefined();
|
|
||||||
expect(apiInstance.languages).toBeDefined();
|
|
||||||
expect(apiInstance.registrations).toBeDefined();
|
|
||||||
expect(apiInstance.rules).toBeDefined();
|
|
||||||
expect(apiInstance.stats).toBeDefined();
|
|
||||||
expect(apiInstance.urls).toBeDefined();
|
|
||||||
expect(apiInstance.max_toot_chars).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
@ -40,3 +21,4 @@ afterAll(async () => {
|
||||||
|
|
||||||
await AppDataSource.destroy();
|
await AppDataSource.destroy();
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ export interface LysandAction extends LysandObjectType {
|
||||||
| "FollowAccept"
|
| "FollowAccept"
|
||||||
| "FollowReject"
|
| "FollowReject"
|
||||||
| "Announce"
|
| "Announce"
|
||||||
| "Undo";
|
| "Undo"
|
||||||
|
| "Extension";
|
||||||
author: string;
|
author: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
19
utils/content_types.ts
Normal file
19
utils/content_types.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ContentFormat } from "~types/lysand/Object";
|
||||||
|
|
||||||
|
export const getBestContentType = (contents: ContentFormat[]) => {
|
||||||
|
// Find the best content and content type
|
||||||
|
if (contents.find(c => c.content_type === "text/x.misskeymarkdown")) {
|
||||||
|
return (
|
||||||
|
contents.find(c => c.content_type === "text/x.misskeymarkdown") ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
} else if (contents.find(c => c.content_type === "text/html")) {
|
||||||
|
return contents.find(c => c.content_type === "text/html") || null;
|
||||||
|
} else if (contents.find(c => c.content_type === "text/markdown")) {
|
||||||
|
return contents.find(c => c.content_type === "text/markdown") || null;
|
||||||
|
} else if (contents.find(c => c.content_type === "text/plain")) {
|
||||||
|
return contents.find(c => c.content_type === "text/plain") || null;
|
||||||
|
} else {
|
||||||
|
return contents[0] || null;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue