mirror of
https://github.com/versia-pub/server.git
synced 2025-12-07 00:48:18 +01:00
Media upload for avatars and banners, more work, fix tests
This commit is contained in:
parent
16cfd5d900
commit
460b68c381
169
classes/media.ts
169
classes/media.ts
|
|
@ -5,7 +5,7 @@ import {
|
||||||
S3Client,
|
S3Client,
|
||||||
} from "@aws-sdk/client-s3";
|
} from "@aws-sdk/client-s3";
|
||||||
import { ConfigType } from "@config";
|
import { ConfigType } from "@config";
|
||||||
|
import sharp from "sharp";
|
||||||
class MediaBackend {
|
class MediaBackend {
|
||||||
backend: string;
|
backend: string;
|
||||||
|
|
||||||
|
|
@ -16,26 +16,98 @@ class MediaBackend {
|
||||||
/**
|
/**
|
||||||
* Adds media to the media backend
|
* Adds media to the media backend
|
||||||
* @param media
|
* @param media
|
||||||
* @returns The hash of the file in SHA-256 (hex format)
|
* @returns The hash of the file in SHA-256 (hex format) with the file extension added to it
|
||||||
*/
|
*/
|
||||||
async addMedia(media: File) {
|
async addMedia(media: File) {
|
||||||
const hash = new Bun.SHA256()
|
const hash = new Bun.SHA256()
|
||||||
.update(await media.arrayBuffer())
|
.update(await media.arrayBuffer())
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
return hash;
|
return `${hash}.${media.name.split(".").pop()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async convertMedia(media: File, config: ConfigType) {
|
||||||
|
const sharpCommand = sharp(await media.arrayBuffer());
|
||||||
|
|
||||||
|
// Rename ".jpg" files to ".jpeg" to avoid sharp errors
|
||||||
|
let name = media.name;
|
||||||
|
if (media.name.endsWith(".jpg")) {
|
||||||
|
name = media.name.replace(".jpg", ".jpeg");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileFormatToConvertTo = config.media.conversion.convert_to;
|
||||||
|
|
||||||
|
switch (fileFormatToConvertTo) {
|
||||||
|
case "png":
|
||||||
|
return new File(
|
||||||
|
[(await sharpCommand.png().toBuffer()).buffer],
|
||||||
|
// Replace the file extension with PNG
|
||||||
|
name.replace(/\.[^/.]+$/, ".png"),
|
||||||
|
{
|
||||||
|
type: "image/png",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
case "webp":
|
||||||
|
return new File(
|
||||||
|
[(await sharpCommand.webp().toBuffer()).buffer],
|
||||||
|
// Replace the file extension with WebP
|
||||||
|
name.replace(/\.[^/.]+$/, ".webp"),
|
||||||
|
{
|
||||||
|
type: "image/webp",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
case "jpeg":
|
||||||
|
return new File(
|
||||||
|
[(await sharpCommand.jpeg().toBuffer()).buffer],
|
||||||
|
// Replace the file extension with JPEG
|
||||||
|
name.replace(/\.[^/.]+$/, ".jpeg"),
|
||||||
|
{
|
||||||
|
type: "image/jpeg",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
case "avif":
|
||||||
|
return new File(
|
||||||
|
[(await sharpCommand.avif().toBuffer()).buffer],
|
||||||
|
// Replace the file extension with AVIF
|
||||||
|
name.replace(/\.[^/.]+$/, ".avif"),
|
||||||
|
{
|
||||||
|
type: "image/avif",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Needs special build of libvips
|
||||||
|
case "jxl":
|
||||||
|
return new File(
|
||||||
|
[(await sharpCommand.jxl().toBuffer()).buffer],
|
||||||
|
// Replace the file extension with JXL
|
||||||
|
name.replace(/\.[^/.]+$/, ".jxl"),
|
||||||
|
{
|
||||||
|
type: "image/jxl",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
case "heif":
|
||||||
|
return new File(
|
||||||
|
[(await sharpCommand.heif().toBuffer()).buffer],
|
||||||
|
// Replace the file extension with HEIF
|
||||||
|
name.replace(/\.[^/.]+$/, ".heif"),
|
||||||
|
{
|
||||||
|
type: "image/heif",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves element from media backend by hash
|
* Retrieves element from media backend by hash
|
||||||
* @param hash The hash of the element in SHA-256 hex format
|
* @param hash The hash of the element in SHA-256 hex format
|
||||||
|
* @param extension The extension of the file
|
||||||
* @returns The file as a File object
|
* @returns The file as a File object
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
||||||
async getMediaByHash(
|
async getMediaByHash(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
hash: string,
|
hash: string
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
extension: string
|
|
||||||
): Promise<File | null> {
|
): Promise<File | null> {
|
||||||
return new File([], "test");
|
return new File([], "test");
|
||||||
}
|
}
|
||||||
|
|
@ -45,35 +117,29 @@ class MediaBackend {
|
||||||
* S3 Backend, stores files in S3
|
* S3 Backend, stores files in S3
|
||||||
*/
|
*/
|
||||||
export class S3Backend extends MediaBackend {
|
export class S3Backend extends MediaBackend {
|
||||||
endpoint: string;
|
|
||||||
bucket: string;
|
|
||||||
region: string;
|
|
||||||
accessKey: string;
|
|
||||||
secretKey: string;
|
|
||||||
publicUrl: string;
|
|
||||||
client: S3Client;
|
client: S3Client;
|
||||||
|
config: ConfigType;
|
||||||
|
|
||||||
constructor(config: ConfigType) {
|
constructor(config: ConfigType) {
|
||||||
super("s3");
|
super("s3");
|
||||||
|
|
||||||
this.endpoint = config.s3.endpoint;
|
this.config = config;
|
||||||
this.bucket = config.s3.bucket_name;
|
|
||||||
this.region = config.s3.region;
|
|
||||||
this.accessKey = config.s3.access_key;
|
|
||||||
this.secretKey = config.s3.secret_access_key;
|
|
||||||
this.publicUrl = config.s3.public_url;
|
|
||||||
|
|
||||||
this.client = new S3Client({
|
this.client = new S3Client({
|
||||||
endpoint: this.endpoint,
|
endpoint: this.config.s3.endpoint,
|
||||||
region: this.region || "auto",
|
region: this.config.s3.region || "auto",
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: this.accessKey,
|
accessKeyId: this.config.s3.access_key,
|
||||||
secretAccessKey: this.secretKey,
|
secretAccessKey: this.config.s3.secret_access_key,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMedia(media: File): Promise<string> {
|
async addMedia(media: File): Promise<string> {
|
||||||
|
if (this.config.media.conversion.convert_images) {
|
||||||
|
media = await this.convertMedia(media, this.config);
|
||||||
|
}
|
||||||
|
|
||||||
const hash = await super.addMedia(media);
|
const hash = await super.addMedia(media);
|
||||||
|
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
|
|
@ -81,10 +147,7 @@ export class S3Backend extends MediaBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file is already present
|
// Check if file is already present
|
||||||
const existingFile = await this.getMediaByHash(
|
const existingFile = await this.getMediaByHash(hash);
|
||||||
hash,
|
|
||||||
media.name.split(".").pop() || ""
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingFile) {
|
if (existingFile) {
|
||||||
// File already exists, so return the hash without uploading it
|
// File already exists, so return the hash without uploading it
|
||||||
|
|
@ -92,7 +155,7 @@ export class S3Backend extends MediaBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: this.bucket,
|
Bucket: this.config.s3.bucket_name,
|
||||||
Key: hash,
|
Key: hash,
|
||||||
Body: Buffer.from(await media.arrayBuffer()),
|
Body: Buffer.from(await media.arrayBuffer()),
|
||||||
ContentType: media.type,
|
ContentType: media.type,
|
||||||
|
|
@ -111,12 +174,9 @@ export class S3Backend extends MediaBackend {
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMediaByHash(
|
async getMediaByHash(hash: string): Promise<File | null> {
|
||||||
hash: string,
|
|
||||||
extension: string
|
|
||||||
): Promise<File | null> {
|
|
||||||
const command = new GetObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
Bucket: this.bucket,
|
Bucket: this.config.s3.bucket_name,
|
||||||
Key: hash,
|
Key: hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -138,7 +198,7 @@ export class S3Backend extends MediaBackend {
|
||||||
throw new Error("Failed to get file");
|
throw new Error("Failed to get file");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new File([body], `${hash}.${extension}`, {
|
return new File([body], hash, {
|
||||||
type: response.ContentType,
|
type: response.ContentType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -148,11 +208,19 @@ export class S3Backend extends MediaBackend {
|
||||||
* Local backend, stores files on filesystem
|
* Local backend, stores files on filesystem
|
||||||
*/
|
*/
|
||||||
export class LocalBackend extends MediaBackend {
|
export class LocalBackend extends MediaBackend {
|
||||||
constructor() {
|
config: ConfigType;
|
||||||
|
|
||||||
|
constructor(config: ConfigType) {
|
||||||
super("local");
|
super("local");
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addMedia(media: File): Promise<string> {
|
async addMedia(media: File): Promise<string> {
|
||||||
|
if (this.config.media.conversion.convert_images) {
|
||||||
|
media = await this.convertMedia(media, this.config);
|
||||||
|
}
|
||||||
|
|
||||||
const hash = await super.addMedia(media);
|
const hash = await super.addMedia(media);
|
||||||
|
|
||||||
await Bun.write(Bun.file(`${process.cwd()}/uploads/${hash}`), media);
|
await Bun.write(Bun.file(`${process.cwd()}/uploads/${hash}`), media);
|
||||||
|
|
@ -160,18 +228,41 @@ export class LocalBackend extends MediaBackend {
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMediaByHash(
|
async getMediaByHash(hash: string): Promise<File | null> {
|
||||||
hash: string,
|
|
||||||
extension: string
|
|
||||||
): Promise<File | null> {
|
|
||||||
const file = Bun.file(`${process.cwd()}/uploads/${hash}`);
|
const file = Bun.file(`${process.cwd()}/uploads/${hash}`);
|
||||||
|
|
||||||
if (!(await file.exists())) {
|
if (!(await file.exists())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new File([await file.arrayBuffer()], `${hash}.${extension}`, {
|
return new File([await file.arrayBuffer()], `${hash}`, {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const uploadFile = (file: File, config: ConfigType) => {
|
||||||
|
const backend = config.media.backend;
|
||||||
|
|
||||||
|
if (backend === "local") {
|
||||||
|
return new LocalBackend(config).addMedia(file);
|
||||||
|
} else if (backend === "s3") {
|
||||||
|
return new S3Backend(config).addMedia(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFile = (
|
||||||
|
hash: string,
|
||||||
|
extension: string,
|
||||||
|
config: ConfigType
|
||||||
|
) => {
|
||||||
|
const backend = config.media.backend;
|
||||||
|
|
||||||
|
if (backend === "local") {
|
||||||
|
return new LocalBackend(config).getMediaByHash(hash);
|
||||||
|
} else if (backend === "s3") {
|
||||||
|
return new S3Backend(config).getMediaByHash(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,17 @@ tls = true
|
||||||
|
|
||||||
[media]
|
[media]
|
||||||
# Can be "s3" or "local", where "local" uploads the file to the local filesystem
|
# Can be "s3" or "local", where "local" uploads the file to the local filesystem
|
||||||
backend = "s3" # NOT IMPLEMENTED
|
# If you need to change this value after setting up your instance, you must move all the files
|
||||||
|
# from one backend to the other manually
|
||||||
|
backend = "s3"
|
||||||
# Whether to check the hash of media when uploading to avoid duplication
|
# Whether to check the hash of media when uploading to avoid duplication
|
||||||
deduplicate_media = true # NOT IMPLEMENTED
|
deduplicate_media = true
|
||||||
|
|
||||||
[media.conversion]
|
[media.conversion]
|
||||||
convert_images = false # NOT IMPLEMENTED
|
convert_images = false
|
||||||
# Can be: "jxl", "webp", "avif", "png", "jpg", "gif"
|
# Can be: "jxl", "webp", "avif", "png", "jpg", "heif"
|
||||||
convert_to = "webp" # NOT IMPLEMENTED
|
# JXL support will likely not work
|
||||||
|
convert_to = "webp"
|
||||||
|
|
||||||
[s3]
|
[s3]
|
||||||
# Can be left blank if you don't use the S3 media backend
|
# Can be left blank if you don't use the S3 media backend
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,10 @@ export class RawActor extends BaseEntity {
|
||||||
isLocalUser ? "" : `@${this.getInstanceDomain()}`
|
isLocalUser ? "" : `@${this.getInstanceDomain()}`
|
||||||
}`,
|
}`,
|
||||||
avatar:
|
avatar:
|
||||||
((icon as APImage).url as string | undefined) ??
|
((icon as APImage).url as string | undefined) ||
|
||||||
config.defaults.avatar,
|
config.defaults.avatar,
|
||||||
header:
|
header:
|
||||||
((image as APImage).url as string | undefined) ??
|
((image as APImage).url as string | undefined) ||
|
||||||
config.defaults.header,
|
config.defaults.header,
|
||||||
locked: false,
|
locked: false,
|
||||||
created_at: new Date(published ?? 0).toISOString(),
|
created_at: new Date(published ?? 0).toISOString(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { getConfig } from "@config";
|
import { ConfigType, getConfig } from "@config";
|
||||||
import {
|
import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
|
|
@ -14,7 +14,11 @@ import {
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
import { RawActor } from "./RawActor";
|
import { RawActor } from "./RawActor";
|
||||||
import { APActor, APOrderedCollectionPage } from "activitypub-types";
|
import {
|
||||||
|
APActor,
|
||||||
|
APCollectionPage,
|
||||||
|
APOrderedCollectionPage,
|
||||||
|
} from "activitypub-types";
|
||||||
import { RawObject } from "./RawObject";
|
import { RawObject } from "./RawObject";
|
||||||
import { Token } from "./Token";
|
import { Token } from "./Token";
|
||||||
import { Status } from "./Status";
|
import { Status } from "./Status";
|
||||||
|
|
@ -90,13 +94,13 @@ export class User extends BaseEntity {
|
||||||
source!: APISource;
|
source!: APISource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The avatar for the user.
|
* The avatar for the user (filename, as UUID)
|
||||||
*/
|
*/
|
||||||
@Column("varchar")
|
@Column("varchar")
|
||||||
avatar!: string;
|
avatar!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The header for the user.
|
* The header for the user (filename, as UUID)
|
||||||
*/
|
*/
|
||||||
@Column("varchar")
|
@Column("varchar")
|
||||||
header!: string;
|
header!: string;
|
||||||
|
|
@ -156,6 +160,32 @@ export class User extends BaseEntity {
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
pinned_notes!: RawObject[];
|
pinned_notes!: RawObject[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's avatar in raw URL format
|
||||||
|
* @param config The config to use
|
||||||
|
* @returns The raw URL for the user's avatar
|
||||||
|
*/
|
||||||
|
getAvatarUrl(config: ConfigType) {
|
||||||
|
if (config.media.backend === "local") {
|
||||||
|
return `${config.http.base_url}/media/${this.avatar}`;
|
||||||
|
} else if (config.media.backend === "s3") {
|
||||||
|
return `${config.s3.public_url}/${this.avatar}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the user's header in raw URL format
|
||||||
|
* @param config The config to use
|
||||||
|
* @returns The raw URL for the user's header
|
||||||
|
*/
|
||||||
|
getHeaderUrl(config: ConfigType) {
|
||||||
|
if (config.media.backend === "local") {
|
||||||
|
return `${config.http.base_url}/media/${this.header}`;
|
||||||
|
} else if (config.media.backend === "s3") {
|
||||||
|
return `${config.s3.public_url}/${this.header}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async getFromRequest(req: Request) {
|
static async getFromRequest(req: Request) {
|
||||||
// Check auth token
|
// Check auth token
|
||||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
||||||
|
|
@ -196,7 +226,7 @@ export class User extends BaseEntity {
|
||||||
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
||||||
followers = await fetch((followers.next as string).toString(), {
|
followers = await fetch((followers.next as string).toString(), {
|
||||||
headers: { Accept: "application/activity+json" },
|
headers: { Accept: "application/activity+json" },
|
||||||
}).then(res => res.json());
|
}).then(res => res.json() as APCollectionPage);
|
||||||
|
|
||||||
followersList = {
|
followersList = {
|
||||||
...followersList,
|
...followersList,
|
||||||
|
|
@ -408,11 +438,11 @@ export class User extends BaseEntity {
|
||||||
summary: this.note,
|
summary: this.note,
|
||||||
icon: {
|
icon: {
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: this.avatar,
|
url: this.getAvatarUrl(config),
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: this.header,
|
url: this.getHeaderUrl(config),
|
||||||
},
|
},
|
||||||
publicKey: {
|
publicKey: {
|
||||||
id: `${config.http.base_url}/users/${this.username}/actor#main-key`,
|
id: `${config.http.base_url}/users/${this.username}/actor#main-key`,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@
|
||||||
"dev": "bun run index.ts",
|
"dev": "bun run index.ts",
|
||||||
"start": "bun run index.ts"
|
"start": "bun run index.ts"
|
||||||
},
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"sharp"
|
||||||
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@julr/unocss-preset-forms": "^0.0.5",
|
"@julr/unocss-preset-forms": "^0.0.5",
|
||||||
"@types/jsonld": "^1.5.9",
|
"@types/jsonld": "^1.5.9",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { User } from "~database/entities/User";
|
||||||
import { applyConfig } from "@api";
|
import { applyConfig } from "@api";
|
||||||
import { sanitize } from "isomorphic-dompurify";
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
import { sanitizeHtml } from "@sanitization";
|
import { sanitizeHtml } from "@sanitization";
|
||||||
|
import { uploadFile } from "~classes/media";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["PATCH"],
|
allowedMethods: ["PATCH"],
|
||||||
|
|
@ -145,7 +146,9 @@ export default async (req: Request): Promise<Response> => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Store the file somewhere and then change the user's actual avatar
|
const hash = await uploadFile(avatar, config);
|
||||||
|
|
||||||
|
user.avatar = hash || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
|
|
@ -156,7 +159,10 @@ export default async (req: Request): Promise<Response> => {
|
||||||
422
|
422
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// TODO: Store the file somewhere and then change the user's actual header
|
|
||||||
|
const hash = await uploadFile(header, config);
|
||||||
|
|
||||||
|
user.header = hash || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locked) {
|
if (locked) {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ export default async (): Promise<Response> => {
|
||||||
characters_reserved_per_url: 0,
|
characters_reserved_per_url: 0,
|
||||||
max_characters: config.validation.max_note_size,
|
max_characters: config.validation.max_note_size,
|
||||||
max_media_attachments: config.validation.max_media_attachments,
|
max_media_attachments: config.validation.max_media_attachments,
|
||||||
|
supported_mime_types: [
|
||||||
|
"text/plain",
|
||||||
|
"text/markdown",
|
||||||
|
"text/html",
|
||||||
|
"text/x.misskeymarkdown",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: "A test instance",
|
description: "A test instance",
|
||||||
|
|
@ -67,7 +73,43 @@ export default async (): Promise<Response> => {
|
||||||
urls: {
|
urls: {
|
||||||
streaming_api: "",
|
streaming_api: "",
|
||||||
},
|
},
|
||||||
version: "0.0.1",
|
version: "4.2.0+glitch (compatible; Lysand 0.0.1)",
|
||||||
max_toot_chars: config.validation.max_note_size,
|
max_toot_chars: config.validation.max_note_size,
|
||||||
|
pleroma: {
|
||||||
|
metadata: {
|
||||||
|
// account_activation_required: false,
|
||||||
|
features: [
|
||||||
|
"pleroma_api",
|
||||||
|
"akkoma_api",
|
||||||
|
"mastodon_api",
|
||||||
|
// "mastodon_api_streaming",
|
||||||
|
// "polls",
|
||||||
|
// "v2_suggestions",
|
||||||
|
// "pleroma_explicit_addressing",
|
||||||
|
// "shareable_emoji_packs",
|
||||||
|
// "multifetch",
|
||||||
|
// "pleroma:api/v1/notifications:include_types_filter",
|
||||||
|
"quote_posting",
|
||||||
|
"editing",
|
||||||
|
// "bubble_timeline",
|
||||||
|
// "relay",
|
||||||
|
// "pleroma_emoji_reactions",
|
||||||
|
// "exposable_reactions",
|
||||||
|
// "profile_directory",
|
||||||
|
// "custom_emoji_reactions",
|
||||||
|
// "pleroma:get:main/ostatus",
|
||||||
|
],
|
||||||
|
post_formats: [
|
||||||
|
"text/plain",
|
||||||
|
"text/html",
|
||||||
|
"text/markdown",
|
||||||
|
"text/x.misskeymarkdown",
|
||||||
|
],
|
||||||
|
privileged_staff: false,
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
mau: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ export default async (
|
||||||
|
|
||||||
const { user } = await User.getFromRequest(req);
|
const { user } = await User.getFromRequest(req);
|
||||||
|
|
||||||
// TODO: Add checks for user's permissions to view this status
|
|
||||||
|
|
||||||
let foundStatus: RawObject | null;
|
let foundStatus: RawObject | null;
|
||||||
try {
|
try {
|
||||||
foundStatus = await RawObject.findOneBy({
|
foundStatus = await RawObject.findOneBy({
|
||||||
|
|
@ -43,6 +41,14 @@ export default async (
|
||||||
|
|
||||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
// Check if user is authorized to view this status (if it's private)
|
||||||
|
if (
|
||||||
|
(await foundStatus.toAPI()).visibility === "private" &&
|
||||||
|
(await foundStatus.toAPI()).account.id !== user?.id
|
||||||
|
) {
|
||||||
|
return errorResponse("Record not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
return jsonResponse(await foundStatus.toAPI());
|
return jsonResponse(await foundStatus.toAPI());
|
||||||
} else if (req.method === "DELETE") {
|
} else if (req.method === "DELETE") {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { errorResponse, jsonResponse } from "@response";
|
||||||
import { sanitizeHtml } from "@sanitization";
|
import { sanitizeHtml } from "@sanitization";
|
||||||
import { APActor } from "activitypub-types";
|
import { APActor } from "activitypub-types";
|
||||||
import { sanitize } from "isomorphic-dompurify";
|
import { sanitize } from "isomorphic-dompurify";
|
||||||
|
import { parse } from "marked";
|
||||||
import { Application } from "~database/entities/Application";
|
import { Application } from "~database/entities/Application";
|
||||||
import { RawObject } from "~database/entities/RawObject";
|
import { RawObject } from "~database/entities/RawObject";
|
||||||
import { Status } from "~database/entities/Status";
|
import { Status } from "~database/entities/Status";
|
||||||
|
|
@ -50,6 +51,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
sensitive,
|
sensitive,
|
||||||
spoiler_text,
|
spoiler_text,
|
||||||
visibility,
|
visibility,
|
||||||
|
content_type,
|
||||||
} = await parseRequest<{
|
} = await parseRequest<{
|
||||||
status: string;
|
status: string;
|
||||||
media_ids?: string[];
|
media_ids?: string[];
|
||||||
|
|
@ -67,14 +69,22 @@ export default async (req: Request): Promise<Response> => {
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
}>(req);
|
}>(req);
|
||||||
|
|
||||||
// TODO: Parse Markdown statuses
|
|
||||||
|
|
||||||
// Validate status
|
// Validate status
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return errorResponse("Status is required", 422);
|
return errorResponse("Status is required", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedStatus = await sanitizeHtml(status);
|
let sanitizedStatus: string;
|
||||||
|
|
||||||
|
if (content_type === "text/markdown") {
|
||||||
|
sanitizedStatus = await sanitizeHtml(parse(status));
|
||||||
|
} else if (content_type === "text/x.misskeymarkdown") {
|
||||||
|
// Parse as MFM
|
||||||
|
// TODO: Parse as MFM
|
||||||
|
sanitizedStatus = await sanitizeHtml(parse(status));
|
||||||
|
} else {
|
||||||
|
sanitizedStatus = await sanitizeHtml(status);
|
||||||
|
}
|
||||||
|
|
||||||
if (sanitizedStatus.length > config.validation.max_note_size) {
|
if (sanitizedStatus.length > config.validation.max_note_size) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
|
|
|
||||||
|
|
@ -23,27 +23,34 @@ beforeAll(async () => {
|
||||||
|
|
||||||
describe("POST /@test/actor", () => {
|
describe("POST /@test/actor", () => {
|
||||||
test("should return a valid ActivityPub Actor when querying an existing user", async () => {
|
test("should return a valid ActivityPub Actor when querying an existing user", async () => {
|
||||||
const response = await fetch(`${config.http.base_url}/users/test/actor`, {
|
const response = await fetch(
|
||||||
method: "GET",
|
`${config.http.base_url}/users/test/actor`,
|
||||||
headers: {
|
{
|
||||||
Accept: "application/activity+json",
|
method: "GET",
|
||||||
},
|
headers: {
|
||||||
});
|
Accept: "application/activity+json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get("content-type")).toBe(
|
expect(response.headers.get("content-type")).toBe(
|
||||||
"application/activity+json"
|
"application/activity+json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const actor: APActor = await response.json();
|
const actor = (await response.json()) as APActor;
|
||||||
|
|
||||||
expect(actor.type).toBe("Person");
|
expect(actor.type).toBe("Person");
|
||||||
expect(actor.id).toBe(`${config.http.base_url}/users/test`);
|
expect(actor.id).toBe(`${config.http.base_url}/users/test`);
|
||||||
expect(actor.preferredUsername).toBe("test");
|
expect(actor.preferredUsername).toBe("test");
|
||||||
expect(actor.inbox).toBe(`${config.http.base_url}/users/test/inbox`);
|
expect(actor.inbox).toBe(`${config.http.base_url}/users/test/inbox`);
|
||||||
expect(actor.outbox).toBe(`${config.http.base_url}/users/test/outbox`);
|
expect(actor.outbox).toBe(`${config.http.base_url}/users/test/outbox`);
|
||||||
expect(actor.followers).toBe(`${config.http.base_url}/users/test/followers`);
|
expect(actor.followers).toBe(
|
||||||
expect(actor.following).toBe(`${config.http.base_url}/users/test/following`);
|
`${config.http.base_url}/users/test/followers`
|
||||||
|
);
|
||||||
|
expect(actor.following).toBe(
|
||||||
|
`${config.http.base_url}/users/test/following`
|
||||||
|
);
|
||||||
expect((actor as any).publicKey).toBeDefined();
|
expect((actor as any).publicKey).toBeDefined();
|
||||||
expect((actor as any).publicKey.id).toBeDefined();
|
expect((actor as any).publicKey.id).toBeDefined();
|
||||||
expect((actor as any).publicKey.owner).toBe(
|
expect((actor as any).publicKey.owner).toBe(
|
||||||
|
|
@ -82,4 +89,6 @@ afterAll(async () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
await user.remove();
|
await user.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await AppDataSource.destroy();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,28 @@ describe("API Tests", () => {
|
||||||
token = await token.save();
|
token = await token.save();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
|
.where("activity.data->>'actor' = :actor", {
|
||||||
|
actor: `${config.http.base_url}/users/test`,
|
||||||
|
})
|
||||||
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
// Delete all created objects and activities as part of testing
|
||||||
|
for (const activity of activities) {
|
||||||
|
for (const object of activity.objects) {
|
||||||
|
await object.remove();
|
||||||
|
}
|
||||||
|
await activity.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.remove();
|
||||||
|
await user2.remove();
|
||||||
|
|
||||||
|
await AppDataSource.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
describe("POST /api/v1/accounts/:id", () => {
|
describe("POST /api/v1/accounts/:id", () => {
|
||||||
test("should return a 404 error when trying to fetch a non-existent user", async () => {
|
test("should return a 404 error when trying to fetch a non-existent user", async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
@ -150,7 +172,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const statuses: APIStatus[] = await response.json();
|
const statuses = (await response.json()) as APIStatus[];
|
||||||
|
|
||||||
expect(statuses.some(s => s.id === status?.id)).toBe(true);
|
expect(statuses.some(s => s.id === status?.id)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -177,7 +199,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const user: APIAccount = await response.json();
|
const user = (await response.json()) as APIAccount;
|
||||||
|
|
||||||
expect(user.display_name).toBe("New Display Name");
|
expect(user.display_name).toBe("New Display Name");
|
||||||
});
|
});
|
||||||
|
|
@ -201,7 +223,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIAccount = await response.json();
|
const account = (await response.json()) as APIAccount;
|
||||||
|
|
||||||
expect(account.username).toBe(user.username);
|
expect(account.username).toBe(user.username);
|
||||||
expect(account.bot).toBe(false);
|
expect(account.bot).toBe(false);
|
||||||
|
|
@ -246,7 +268,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const statuses: APIStatus[] = await response.json();
|
const statuses = (await response.json()) as APIStatus[];
|
||||||
|
|
||||||
expect(statuses.length).toBe(1);
|
expect(statuses.length).toBe(1);
|
||||||
|
|
||||||
|
|
@ -278,7 +300,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.following).toBe(true);
|
expect(account.following).toBe(true);
|
||||||
|
|
@ -304,7 +326,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.following).toBe(false);
|
expect(account.following).toBe(false);
|
||||||
|
|
@ -330,7 +352,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.followed_by).toBe(false);
|
expect(account.followed_by).toBe(false);
|
||||||
|
|
@ -356,7 +378,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.blocking).toBe(true);
|
expect(account.blocking).toBe(true);
|
||||||
|
|
@ -382,7 +404,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.blocking).toBe(false);
|
expect(account.blocking).toBe(false);
|
||||||
|
|
@ -408,7 +430,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.muting).toBe(true);
|
expect(account.muting).toBe(true);
|
||||||
|
|
@ -433,7 +455,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.muting).toBe(true);
|
expect(account.muting).toBe(true);
|
||||||
|
|
@ -460,7 +482,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.muting).toBe(false);
|
expect(account.muting).toBe(false);
|
||||||
|
|
@ -486,7 +508,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.endorsed).toBe(true);
|
expect(account.endorsed).toBe(true);
|
||||||
|
|
@ -512,7 +534,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIRelationship = await response.json();
|
const account = (await response.json()) as APIRelationship;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.endorsed).toBe(false);
|
expect(account.endorsed).toBe(false);
|
||||||
|
|
@ -538,7 +560,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const account: APIAccount = await response.json();
|
const account = (await response.json()) as APIAccount;
|
||||||
|
|
||||||
expect(account.id).toBe(user2.id);
|
expect(account.id).toBe(user2.id);
|
||||||
expect(account.note).toBe("This is a new note");
|
expect(account.note).toBe("This is a new note");
|
||||||
|
|
@ -562,7 +584,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const relationships: APIRelationship[] = await response.json();
|
const relationships = (await response.json()) as APIRelationship[];
|
||||||
|
|
||||||
expect(Array.isArray(relationships)).toBe(true);
|
expect(Array.isArray(relationships)).toBe(true);
|
||||||
expect(relationships.length).toBeGreaterThan(0);
|
expect(relationships.length).toBeGreaterThan(0);
|
||||||
|
|
@ -595,8 +617,10 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const familiarFollowers: { id: string; accounts: APIAccount[] }[] =
|
const familiarFollowers = (await response.json()) as {
|
||||||
await response.json();
|
id: string;
|
||||||
|
accounts: APIAccount[];
|
||||||
|
}[];
|
||||||
|
|
||||||
expect(Array.isArray(familiarFollowers)).toBe(true);
|
expect(Array.isArray(familiarFollowers)).toBe(true);
|
||||||
expect(familiarFollowers.length).toBeGreaterThan(0);
|
expect(familiarFollowers.length).toBeGreaterThan(0);
|
||||||
|
|
@ -657,7 +681,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusJson = await response.json();
|
const statusJson = (await response.json()) as APIStatus;
|
||||||
|
|
||||||
expect(statusJson.id).toBe(status?.id);
|
expect(statusJson.id).toBe(status?.id);
|
||||||
expect(statusJson.content).toBeDefined();
|
expect(statusJson.content).toBeDefined();
|
||||||
|
|
@ -718,7 +742,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const instance: APIInstance = await response.json();
|
const instance = (await response.json()) as APIInstance;
|
||||||
|
|
||||||
expect(instance.uri).toBe(new URL(config.http.base_url).hostname);
|
expect(instance.uri).toBe(new URL(config.http.base_url).hostname);
|
||||||
expect(instance.title).toBeDefined();
|
expect(instance.title).toBeDefined();
|
||||||
|
|
@ -764,7 +788,7 @@ describe("API Tests", () => {
|
||||||
"application/json"
|
"application/json"
|
||||||
);
|
);
|
||||||
|
|
||||||
const emojis: APIEmoji[] = await response.json();
|
const emojis = (await response.json()) as APIEmoji[];
|
||||||
|
|
||||||
expect(emojis.length).toBeGreaterThan(0);
|
expect(emojis.length).toBeGreaterThan(0);
|
||||||
expect(emojis[0].shortcode).toBe("test");
|
expect(emojis[0].shortcode).toBe("test");
|
||||||
|
|
@ -774,26 +798,4 @@ describe("API Tests", () => {
|
||||||
await Emoji.delete({ shortcode: "test" });
|
await Emoji.delete({ shortcode: "test" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
const activities = await RawActivity.createQueryBuilder("activity")
|
|
||||||
.where("activity.data->>'actor' = :actor", {
|
|
||||||
actor: `${config.http.base_url}/users/test`,
|
|
||||||
})
|
|
||||||
.leftJoinAndSelect("activity.objects", "objects")
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
// Delete all created objects and activities as part of testing
|
|
||||||
await Promise.all(
|
|
||||||
activities.map(async activity => {
|
|
||||||
await Promise.all(
|
|
||||||
activity.objects.map(async object => await object.remove())
|
|
||||||
);
|
|
||||||
await activity.remove();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.remove();
|
|
||||||
await user2.remove();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,6 @@ describe("Instance", () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await instance.remove();
|
await instance.remove();
|
||||||
|
|
||||||
|
await AppDataSource.destroy();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,27 @@
|
||||||
import { getConfig } from "@config";
|
import { ConfigType, getConfig } from "@config";
|
||||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { LocalBackend, S3Backend } from "~classes/media";
|
import { LocalBackend, S3Backend } from "~classes/media";
|
||||||
import { unlink } from "fs/promises";
|
import { unlink } from "fs/promises";
|
||||||
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
|
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||||
|
|
||||||
const config = getConfig();
|
const originalConfig = getConfig();
|
||||||
|
const modifiedConfig: ConfigType = {
|
||||||
|
...originalConfig,
|
||||||
|
media: {
|
||||||
|
...originalConfig.media,
|
||||||
|
conversion: {
|
||||||
|
...originalConfig.media.conversion,
|
||||||
|
convert_images: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe("LocalBackend", () => {
|
describe("LocalBackend", () => {
|
||||||
let localBackend: LocalBackend;
|
let localBackend: LocalBackend;
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
localBackend = new LocalBackend();
|
localBackend = new LocalBackend(modifiedConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
@ -25,7 +35,7 @@ describe("LocalBackend", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hash = await localBackend.addMedia(media);
|
const hash = await localBackend.addMedia(media);
|
||||||
fileName = `${hash}`;
|
fileName = hash;
|
||||||
|
|
||||||
expect(hash).toBeDefined();
|
expect(hash).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -33,16 +43,14 @@ describe("LocalBackend", () => {
|
||||||
|
|
||||||
describe("getMediaByHash", () => {
|
describe("getMediaByHash", () => {
|
||||||
it("should retrieve the file from the local filesystem and return it as a File object", async () => {
|
it("should retrieve the file from the local filesystem and return it as a File object", async () => {
|
||||||
const media = await localBackend.getMediaByHash(fileName, "txt");
|
const media = await localBackend.getMediaByHash(fileName);
|
||||||
|
|
||||||
expect(media).toBeInstanceOf(File);
|
expect(media).toBeInstanceOf(File);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return null if the file does not exist", async () => {
|
it("should return null if the file does not exist", async () => {
|
||||||
const media = await localBackend.getMediaByHash(
|
const media =
|
||||||
"does-not-exist",
|
await localBackend.getMediaByHash("does-not-exist.txt");
|
||||||
"txt"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(media).toBeNull();
|
expect(media).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
@ -50,12 +58,12 @@ describe("LocalBackend", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("S3Backend", () => {
|
describe("S3Backend", () => {
|
||||||
const s3Backend = new S3Backend(config);
|
const s3Backend = new S3Backend(modifiedConfig);
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
const command = new DeleteObjectCommand({
|
const command = new DeleteObjectCommand({
|
||||||
Bucket: config.s3.bucket_name,
|
Bucket: modifiedConfig.s3.bucket_name,
|
||||||
Key: fileName,
|
Key: fileName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,7 +77,7 @@ describe("S3Backend", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hash = await s3Backend.addMedia(media);
|
const hash = await s3Backend.addMedia(media);
|
||||||
fileName = `${hash}`;
|
fileName = hash;
|
||||||
|
|
||||||
expect(hash).toBeDefined();
|
expect(hash).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
@ -77,16 +85,13 @@ describe("S3Backend", () => {
|
||||||
|
|
||||||
describe("getMediaByHash", () => {
|
describe("getMediaByHash", () => {
|
||||||
it("should retrieve the file from the S3 bucket and return it as a File object", async () => {
|
it("should retrieve the file from the S3 bucket and return it as a File object", async () => {
|
||||||
const media = await s3Backend.getMediaByHash(fileName, "txt");
|
const media = await s3Backend.getMediaByHash(fileName);
|
||||||
|
|
||||||
expect(media).toBeInstanceOf(File);
|
expect(media).toBeInstanceOf(File);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return null if the file does not exist", async () => {
|
it("should return null if the file does not exist", async () => {
|
||||||
const media = await s3Backend.getMediaByHash(
|
const media = await s3Backend.getMediaByHash("does-not-exist.txt");
|
||||||
"does-not-exist",
|
|
||||||
"txt"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(media).toBeNull();
|
expect(media).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ describe("POST /@test/inbox", () => {
|
||||||
manuallyApprovesFollowers: false,
|
manuallyApprovesFollowers: false,
|
||||||
followers: `${config.http.base_url}/users/test/followers`,
|
followers: `${config.http.base_url}/users/test/followers`,
|
||||||
following: `${config.http.base_url}/users/test/following`,
|
following: `${config.http.base_url}/users/test/following`,
|
||||||
|
published: expect.any(String),
|
||||||
name: "",
|
name: "",
|
||||||
"@context": [
|
"@context": [
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
|
@ -217,11 +218,11 @@ describe("POST /@test/inbox", () => {
|
||||||
],
|
],
|
||||||
icon: {
|
icon: {
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: "",
|
url: expect.any(String),
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: "",
|
url: expect.any(String),
|
||||||
},
|
},
|
||||||
inbox: `${config.http.base_url}/users/test/inbox`,
|
inbox: `${config.http.base_url}/users/test/inbox`,
|
||||||
type: "Person",
|
type: "Person",
|
||||||
|
|
@ -311,4 +312,6 @@ afterAll(async () => {
|
||||||
await Promise.all(tokens.map(async token => await token.remove()));
|
await Promise.all(tokens.map(async token => await token.remove()));
|
||||||
|
|
||||||
if (user) await user.remove();
|
if (user) await user.remove();
|
||||||
|
|
||||||
|
await AppDataSource.destroy();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ describe("POST /api/v1/apps/", () => {
|
||||||
formData.append("redirect_uris", "https://example.com");
|
formData.append("redirect_uris", "https://example.com");
|
||||||
formData.append("scopes", "read write");
|
formData.append("scopes", "read write");
|
||||||
|
|
||||||
|
// @ts-expect-error FormData works
|
||||||
const response = await fetch(`${config.http.base_url}/api/v1/apps/`, {
|
const response = await fetch(`${config.http.base_url}/api/v1/apps/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
@ -42,7 +43,7 @@ describe("POST /api/v1/apps/", () => {
|
||||||
expect(response.headers.get("content-type")).toBe("application/json");
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const json = await response.json();
|
const json = (await response.json()) as any;
|
||||||
|
|
||||||
expect(json).toEqual({
|
expect(json).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
|
|
@ -67,6 +68,8 @@ describe("POST /auth/login/", () => {
|
||||||
|
|
||||||
formData.append("email", "test@test.com");
|
formData.append("email", "test@test.com");
|
||||||
formData.append("password", "test");
|
formData.append("password", "test");
|
||||||
|
|
||||||
|
// @ts-expect-error FormData works
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${config.http.base_url}/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
`${config.http.base_url}/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||||
{
|
{
|
||||||
|
|
@ -96,13 +99,17 @@ describe("POST /oauth/token/", () => {
|
||||||
formData.append("client_secret", client_secret);
|
formData.append("client_secret", client_secret);
|
||||||
formData.append("scope", "read+write");
|
formData.append("scope", "read+write");
|
||||||
|
|
||||||
|
// @ts-expect-error FormData works
|
||||||
const response = await fetch(`${config.http.base_url}/oauth/token/`, {
|
const response = await fetch(`${config.http.base_url}/oauth/token/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const json = await response.json();
|
const json = (await response.json()) as any;
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get("content-type")).toBe("application/json");
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
@ -134,7 +141,7 @@ describe("GET /api/v1/apps/verify_credentials", () => {
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.headers.get("content-type")).toBe("application/json");
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
const credentials: Partial<Application> = await response.json();
|
const credentials = (await response.json()) as Partial<Application>;
|
||||||
|
|
||||||
expect(credentials.name).toBe("Test Application");
|
expect(credentials.name).toBe("Test Application");
|
||||||
expect(credentials.website).toBe("https://example.com");
|
expect(credentials.website).toBe("https://example.com");
|
||||||
|
|
@ -167,4 +174,6 @@ afterAll(async () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user) await user.remove();
|
if (user) await user.remove();
|
||||||
|
|
||||||
|
await AppDataSource.destroy();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,11 @@ export async function parseRequest<T>(request: Request): Promise<Partial<T>> {
|
||||||
// If a file, set as a file
|
// If a file, set as a file
|
||||||
if (value instanceof File) {
|
if (value instanceof File) {
|
||||||
data[key] = value;
|
data[key] = value;
|
||||||
|
} else {
|
||||||
|
// Otherwise, set as a string
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
data[key] = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, set as a string
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
||||||
data[key] = value.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue