mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
fix(api): 🐛 Fetch media content-type from data, instead of doing naive guesses
This commit is contained in:
parent
6f67881d96
commit
4fdb96930f
|
|
@ -48,6 +48,7 @@ max_emoji_description_size = 1000
|
|||
- Correctly proxy all URIs in custom Markdown text.
|
||||
- Fixed several issues with the [ActivityPub Federation Bridge](https://github.com/versia-pub/activitypub) preventing it from operating properly.
|
||||
- Fixed incorrect content-type on some media when using S3.
|
||||
- All media content-type is now correctly fetched, instead of guessed from the file extension.
|
||||
|
||||
# `0.7.0` • The Auth and APIs Update
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
|
||||
import { mimeLookup } from "@/content_types";
|
||||
import { mergeAndDeduplicate } from "@/lib";
|
||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
|
|
@ -267,21 +268,37 @@ export default apiRoute((app) =>
|
|||
|
||||
if (avatar) {
|
||||
if (avatar instanceof File) {
|
||||
const { path } = await mediaManager.addFile(avatar);
|
||||
const { path, uploadedFile } =
|
||||
await mediaManager.addFile(avatar);
|
||||
const contentType = uploadedFile.type;
|
||||
|
||||
self.avatar = Attachment.getUrl(path);
|
||||
self.source.avatar = {
|
||||
content_type: contentType,
|
||||
};
|
||||
} else {
|
||||
self.avatar = avatar;
|
||||
self.source.avatar = {
|
||||
content_type: await mimeLookup(avatar),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (header) {
|
||||
if (header instanceof File) {
|
||||
const { path } = await mediaManager.addFile(header);
|
||||
const { path, uploadedFile } =
|
||||
await mediaManager.addFile(header);
|
||||
const contentType = uploadedFile.type;
|
||||
|
||||
self.header = Attachment.getUrl(path);
|
||||
self.source.header = {
|
||||
content_type: contentType,
|
||||
};
|
||||
} else {
|
||||
self.header = header;
|
||||
self.source.header = {
|
||||
content_type: await mimeLookup(header),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,12 @@ export default apiRoute((app) =>
|
|||
name: "Versia Server",
|
||||
version: pkg.version,
|
||||
},
|
||||
banner: urlToContentFormat(config.instance.banner),
|
||||
logo: urlToContentFormat(config.instance.logo),
|
||||
banner: config.instance.banner
|
||||
? urlToContentFormat(config.instance.banner)
|
||||
: undefined,
|
||||
logo: config.instance.logo
|
||||
? urlToContentFormat(config.instance.logo)
|
||||
: undefined,
|
||||
shared_inbox: new URL(
|
||||
"/inbox",
|
||||
config.http.base_url,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { WebFinger } from "@versia/federation/schemas";
|
|||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { lookup } from "mime-types";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
|
@ -137,9 +136,8 @@ export default apiRoute((app) =>
|
|||
},
|
||||
{
|
||||
rel: "avatar",
|
||||
// TODO: don't... don't use the mime type from the file extension
|
||||
type:
|
||||
lookup(user.getAvatarUrl(config)) ||
|
||||
user.data.source.avatar?.content_type ||
|
||||
"application/octet-stream",
|
||||
href: user.getAvatarUrl(config),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -125,6 +125,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
avatar: z
|
||||
.object({
|
||||
content_type: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
header: z
|
||||
.object({
|
||||
content_type: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
role: z
|
||||
|
|
@ -691,6 +701,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
user: VersiaUser,
|
||||
instance: Instance,
|
||||
): Promise<User> {
|
||||
const avatar = user.avatar ? Object.entries(user.avatar)[0] : null;
|
||||
const header = user.header ? Object.entries(user.header)[0] : null;
|
||||
|
||||
const data = {
|
||||
username: user.username,
|
||||
uri: user.uri,
|
||||
|
|
@ -708,12 +721,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
fields: user.fields ?? [],
|
||||
updatedAt: new Date(user.created_at).toISOString(),
|
||||
instanceId: instance.id,
|
||||
avatar: user.avatar
|
||||
? Object.entries(user.avatar)[0][1].content
|
||||
: "",
|
||||
header: user.header
|
||||
? Object.entries(user.header)[0][1].content
|
||||
: "",
|
||||
avatar: avatar?.[1].content || "",
|
||||
header: header?.[1].content || "",
|
||||
displayName: user.display_name ?? "",
|
||||
note: getBestContentType(user.bio).content,
|
||||
publicKey: user.public_key.key,
|
||||
|
|
@ -723,6 +732,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
avatar: avatar
|
||||
? {
|
||||
content_type: avatar[0],
|
||||
}
|
||||
: undefined,
|
||||
header: header
|
||||
? {
|
||||
content_type: header[0],
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -840,8 +859,14 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
password: string | undefined;
|
||||
email: string | undefined;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
header?: string;
|
||||
avatar?: {
|
||||
url: string;
|
||||
content_type: string;
|
||||
};
|
||||
header?: {
|
||||
url: string;
|
||||
content_type: string;
|
||||
};
|
||||
admin?: boolean;
|
||||
skipPasswordHash?: boolean;
|
||||
}): Promise<User> {
|
||||
|
|
@ -859,8 +884,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
: await Bun.password.hash(data.password),
|
||||
email: data.email,
|
||||
note: data.bio ?? "",
|
||||
avatar: data.avatar ?? config.defaults.avatar ?? "",
|
||||
header: data.header ?? config.defaults.avatar ?? "",
|
||||
avatar: data.avatar?.url ?? config.defaults.avatar ?? "",
|
||||
header: data.header?.url ?? config.defaults.avatar ?? "",
|
||||
isAdmin: data.admin ?? false,
|
||||
publicKey: keys.public_key,
|
||||
fields: [],
|
||||
|
|
@ -872,6 +897,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
avatar: data.avatar
|
||||
? {
|
||||
content_type: data.avatar.content_type,
|
||||
}
|
||||
: undefined,
|
||||
header: data.header
|
||||
? {
|
||||
content_type: data.header.content_type,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
|
|
@ -1209,8 +1244,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
indexable: false,
|
||||
username: user.username,
|
||||
manually_approves_followers: this.data.isLocked,
|
||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
||||
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
||||
avatar:
|
||||
urlToContentFormat(
|
||||
this.getAvatarUrl(config),
|
||||
this.data.source.avatar?.content_type,
|
||||
) ?? undefined,
|
||||
header:
|
||||
urlToContentFormat(
|
||||
this.getHeaderUrl(config),
|
||||
this.data.source.header?.content_type,
|
||||
) ?? undefined,
|
||||
display_name: user.displayName,
|
||||
fields: user.fields,
|
||||
public_key: {
|
||||
|
|
|
|||
|
|
@ -394,7 +394,16 @@ export const Users = pgTable(
|
|||
inbox: string;
|
||||
outbox: string;
|
||||
}> | null>(),
|
||||
source: jsonb("source").notNull().$type<ApiSource>(),
|
||||
source: jsonb("source").notNull().$type<
|
||||
ApiSource & {
|
||||
avatar?: {
|
||||
content_type: string;
|
||||
};
|
||||
header?: {
|
||||
content_type: string;
|
||||
};
|
||||
}
|
||||
>(),
|
||||
avatar: text("avatar").notNull(),
|
||||
header: text("header").notNull(),
|
||||
createdAt: timestamp("created_at", { precision: 3, mode: "string" })
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { mimeLookup } from "@/content_types.ts";
|
||||
import { randomString } from "@/math.ts";
|
||||
import { setCookie } from "@hono/hono/cookie";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
|
|
@ -245,7 +246,12 @@ export default (plugin: PluginType): void => {
|
|||
const user = await User.fromDataLocal({
|
||||
email: doesEmailExist ? undefined : email,
|
||||
username,
|
||||
avatar: picture,
|
||||
avatar: picture
|
||||
? {
|
||||
url: picture,
|
||||
content_type: await mimeLookup(picture),
|
||||
}
|
||||
: undefined,
|
||||
password: undefined,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ export const getBestContentType = (
|
|||
return { content: "", format: "text/plain" };
|
||||
};
|
||||
|
||||
export const urlToContentFormat = (url?: string): ContentFormat | null => {
|
||||
export const urlToContentFormat = (
|
||||
url: string,
|
||||
contentType?: string,
|
||||
): ContentFormat | null => {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -41,6 +44,7 @@ export const urlToContentFormat = (url?: string): ContentFormat | null => {
|
|||
};
|
||||
}
|
||||
const mimeType =
|
||||
contentType ||
|
||||
lookup(url.replace(new URL(url).search, "")) ||
|
||||
"application/octet-stream";
|
||||
|
||||
|
|
@ -63,7 +67,13 @@ export const mimeLookup = (url: string): Promise<string> => {
|
|||
method: "HEAD",
|
||||
// @ts-expect-error Proxy is a Bun-specific feature
|
||||
proxy: config.http.proxy.address,
|
||||
}).then((response) => response.headers.get("content-type") || "");
|
||||
})
|
||||
.then(
|
||||
(response) =>
|
||||
response.headers.get("content-type") ||
|
||||
"application/octet-stream",
|
||||
)
|
||||
.catch(() => "application/octet-stream");
|
||||
|
||||
return fetchLookup;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue