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.
|
- 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 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.
|
- 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
|
# `0.7.0` • The Auth and APIs Update
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
|
import { apiRoute, applyConfig, auth, jsonOrForm } from "@/api";
|
||||||
|
import { mimeLookup } from "@/content_types";
|
||||||
import { mergeAndDeduplicate } from "@/lib";
|
import { mergeAndDeduplicate } from "@/lib";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
|
|
@ -267,21 +268,37 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
if (avatar instanceof File) {
|
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.avatar = Attachment.getUrl(path);
|
||||||
|
self.source.avatar = {
|
||||||
|
content_type: contentType,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
self.avatar = avatar;
|
self.avatar = avatar;
|
||||||
|
self.source.avatar = {
|
||||||
|
content_type: await mimeLookup(avatar),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
if (header instanceof File) {
|
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.header = Attachment.getUrl(path);
|
||||||
|
self.source.header = {
|
||||||
|
content_type: contentType,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
self.header = header;
|
self.header = header;
|
||||||
|
self.source.header = {
|
||||||
|
content_type: await mimeLookup(header),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,12 @@ export default apiRoute((app) =>
|
||||||
name: "Versia Server",
|
name: "Versia Server",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
},
|
},
|
||||||
banner: urlToContentFormat(config.instance.banner),
|
banner: config.instance.banner
|
||||||
logo: urlToContentFormat(config.instance.logo),
|
? urlToContentFormat(config.instance.banner)
|
||||||
|
: undefined,
|
||||||
|
logo: config.instance.logo
|
||||||
|
? urlToContentFormat(config.instance.logo)
|
||||||
|
: undefined,
|
||||||
shared_inbox: new URL(
|
shared_inbox: new URL(
|
||||||
"/inbox",
|
"/inbox",
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { WebFinger } from "@versia/federation/schemas";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { lookup } from "mime-types";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
@ -137,9 +136,8 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "avatar",
|
rel: "avatar",
|
||||||
// TODO: don't... don't use the mime type from the file extension
|
|
||||||
type:
|
type:
|
||||||
lookup(user.getAvatarUrl(config)) ||
|
user.data.source.avatar?.content_type ||
|
||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
href: user.getAvatarUrl(config),
|
href: user.getAvatarUrl(config),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
avatar: z
|
||||||
|
.object({
|
||||||
|
content_type: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
header: z
|
||||||
|
.object({
|
||||||
|
content_type: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
role: z
|
role: z
|
||||||
|
|
@ -691,6 +701,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
user: VersiaUser,
|
user: VersiaUser,
|
||||||
instance: Instance,
|
instance: Instance,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
|
const avatar = user.avatar ? Object.entries(user.avatar)[0] : null;
|
||||||
|
const header = user.header ? Object.entries(user.header)[0] : null;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
|
|
@ -708,12 +721,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
fields: user.fields ?? [],
|
fields: user.fields ?? [],
|
||||||
updatedAt: new Date(user.created_at).toISOString(),
|
updatedAt: new Date(user.created_at).toISOString(),
|
||||||
instanceId: instance.id,
|
instanceId: instance.id,
|
||||||
avatar: user.avatar
|
avatar: avatar?.[1].content || "",
|
||||||
? Object.entries(user.avatar)[0][1].content
|
header: header?.[1].content || "",
|
||||||
: "",
|
|
||||||
header: user.header
|
|
||||||
? Object.entries(user.header)[0][1].content
|
|
||||||
: "",
|
|
||||||
displayName: user.display_name ?? "",
|
displayName: user.display_name ?? "",
|
||||||
note: getBestContentType(user.bio).content,
|
note: getBestContentType(user.bio).content,
|
||||||
publicKey: user.public_key.key,
|
publicKey: user.public_key.key,
|
||||||
|
|
@ -723,6 +732,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
privacy: "public",
|
privacy: "public",
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
fields: [],
|
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;
|
password: string | undefined;
|
||||||
email: string | undefined;
|
email: string | undefined;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
avatar?: string;
|
avatar?: {
|
||||||
header?: string;
|
url: string;
|
||||||
|
content_type: string;
|
||||||
|
};
|
||||||
|
header?: {
|
||||||
|
url: string;
|
||||||
|
content_type: string;
|
||||||
|
};
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
skipPasswordHash?: boolean;
|
skipPasswordHash?: boolean;
|
||||||
}): Promise<User> {
|
}): Promise<User> {
|
||||||
|
|
@ -859,8 +884,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
: await Bun.password.hash(data.password),
|
: await Bun.password.hash(data.password),
|
||||||
email: data.email,
|
email: data.email,
|
||||||
note: data.bio ?? "",
|
note: data.bio ?? "",
|
||||||
avatar: data.avatar ?? config.defaults.avatar ?? "",
|
avatar: data.avatar?.url ?? config.defaults.avatar ?? "",
|
||||||
header: data.header ?? config.defaults.avatar ?? "",
|
header: data.header?.url ?? config.defaults.avatar ?? "",
|
||||||
isAdmin: data.admin ?? false,
|
isAdmin: data.admin ?? false,
|
||||||
publicKey: keys.public_key,
|
publicKey: keys.public_key,
|
||||||
fields: [],
|
fields: [],
|
||||||
|
|
@ -872,6 +897,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
privacy: "public",
|
privacy: "public",
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
fields: [],
|
fields: [],
|
||||||
|
avatar: data.avatar
|
||||||
|
? {
|
||||||
|
content_type: data.avatar.content_type,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
header: data.header
|
||||||
|
? {
|
||||||
|
content_type: data.header.content_type,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
|
|
@ -1209,8 +1244,16 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
indexable: false,
|
indexable: false,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
manually_approves_followers: this.data.isLocked,
|
manually_approves_followers: this.data.isLocked,
|
||||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
avatar:
|
||||||
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
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,
|
display_name: user.displayName,
|
||||||
fields: user.fields,
|
fields: user.fields,
|
||||||
public_key: {
|
public_key: {
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,16 @@ export const Users = pgTable(
|
||||||
inbox: string;
|
inbox: string;
|
||||||
outbox: string;
|
outbox: string;
|
||||||
}> | null>(),
|
}> | 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(),
|
avatar: text("avatar").notNull(),
|
||||||
header: text("header").notNull(),
|
header: text("header").notNull(),
|
||||||
createdAt: timestamp("created_at", { precision: 3, mode: "string" })
|
createdAt: timestamp("created_at", { precision: 3, mode: "string" })
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { mimeLookup } from "@/content_types.ts";
|
||||||
import { randomString } from "@/math.ts";
|
import { randomString } from "@/math.ts";
|
||||||
import { setCookie } from "@hono/hono/cookie";
|
import { setCookie } from "@hono/hono/cookie";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
|
|
@ -245,7 +246,12 @@ export default (plugin: PluginType): void => {
|
||||||
const user = await User.fromDataLocal({
|
const user = await User.fromDataLocal({
|
||||||
email: doesEmailExist ? undefined : email,
|
email: doesEmailExist ? undefined : email,
|
||||||
username,
|
username,
|
||||||
avatar: picture,
|
avatar: picture
|
||||||
|
? {
|
||||||
|
url: picture,
|
||||||
|
content_type: await mimeLookup(picture),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
password: undefined,
|
password: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@ export const getBestContentType = (
|
||||||
return { content: "", format: "text/plain" };
|
return { content: "", format: "text/plain" };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const urlToContentFormat = (url?: string): ContentFormat | null => {
|
export const urlToContentFormat = (
|
||||||
|
url: string,
|
||||||
|
contentType?: string,
|
||||||
|
): ContentFormat | null => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +44,7 @@ export const urlToContentFormat = (url?: string): ContentFormat | null => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const mimeType =
|
const mimeType =
|
||||||
|
contentType ||
|
||||||
lookup(url.replace(new URL(url).search, "")) ||
|
lookup(url.replace(new URL(url).search, "")) ||
|
||||||
"application/octet-stream";
|
"application/octet-stream";
|
||||||
|
|
||||||
|
|
@ -63,7 +67,13 @@ export const mimeLookup = (url: string): Promise<string> => {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
// @ts-expect-error Proxy is a Bun-specific feature
|
// @ts-expect-error Proxy is a Bun-specific feature
|
||||||
proxy: config.http.proxy.address,
|
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;
|
return fetchLookup;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue