mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Add media attachment functionality to posts
This commit is contained in:
parent
c66e1ac146
commit
28a16e95a4
|
|
@ -62,28 +62,97 @@ max_bio_size = 160
|
||||||
max_note_size = 5000
|
max_note_size = 5000
|
||||||
max_avatar_size = 5_000_000
|
max_avatar_size = 5_000_000
|
||||||
max_header_size = 5_000_000
|
max_header_size = 5_000_000
|
||||||
max_media_size = 40_000_000 # MEDIA NOT IMPLEMENTED
|
max_media_size = 40_000_000
|
||||||
max_media_attachments = 4 # MEDIA NOT IMPLEMENTED
|
max_media_attachments = 10
|
||||||
max_media_description_size = 1000 # MEDIA NOT IMPLEMENTED
|
max_media_description_size = 1000
|
||||||
|
max_poll_options = 20
|
||||||
|
max_poll_option_size = 500
|
||||||
|
min_poll_duration = 60
|
||||||
|
max_poll_duration = 1893456000
|
||||||
max_username_size = 30
|
max_username_size = 30
|
||||||
# An array of strings, defaults are from Akkoma
|
# An array of strings, defaults are from Akkoma
|
||||||
username_blacklist = [ ".well-known", "~", "about", "activities" , "api",
|
username_blacklist = [
|
||||||
"auth", "dev", "inbox", "internal", "main", "media", "nodeinfo", "notice",
|
".well-known",
|
||||||
"oauth", "objects", "proxy", "push", "registration", "relay", "settings",
|
"~",
|
||||||
"status", "tag", "users", "web", "search", "mfa" ]
|
"about",
|
||||||
|
"activities",
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"dev",
|
||||||
|
"inbox",
|
||||||
|
"internal",
|
||||||
|
"main",
|
||||||
|
"media",
|
||||||
|
"nodeinfo",
|
||||||
|
"notice",
|
||||||
|
"oauth",
|
||||||
|
"objects",
|
||||||
|
"proxy",
|
||||||
|
"push",
|
||||||
|
"registration",
|
||||||
|
"relay",
|
||||||
|
"settings",
|
||||||
|
"status",
|
||||||
|
"tag",
|
||||||
|
"users",
|
||||||
|
"web",
|
||||||
|
"search",
|
||||||
|
"mfa",
|
||||||
|
]
|
||||||
# Whether to blacklist known temporary email providers
|
# Whether to blacklist known temporary email providers
|
||||||
blacklist_tempmail = false
|
blacklist_tempmail = false
|
||||||
# Additional email providers to blacklist
|
# Additional email providers to blacklist
|
||||||
email_blacklist = []
|
email_blacklist = []
|
||||||
# Valid URL schemes, otherwise the URL is parsed as text
|
# Valid URL schemes, otherwise the URL is parsed as text
|
||||||
url_scheme_whitelist = [ "http", "https", "ftp", "dat", "dweb", "gopher", "hyper",
|
url_scheme_whitelist = [
|
||||||
"ipfs", "ipns", "irc", "xmpp", "ircs", "magnet", "mailto", "mumble", "ssb",
|
"http",
|
||||||
"gemini" ] # NOT IMPLEMENTED
|
"https",
|
||||||
allowed_mime_types = [ "image/jpeg", "image/png", "image/gif", "image/heic", "image/heif",
|
"ftp",
|
||||||
"image/webp", "image/avif", "video/webm", "video/mp4", "video/quicktime", "video/ogg",
|
"dat",
|
||||||
"audio/wave", "audio/wav", "audio/x-wav", "audio/x-pn-wave", "audio/vnd.wave",
|
"dweb",
|
||||||
"audio/ogg", "audio/vorbis", "audio/mpeg", "audio/mp3", "audio/webm", "audio/flac",
|
"gopher",
|
||||||
"audio/aac", "audio/m4a", "audio/x-m4a", "audio/mp4", "audio/3gpp", "video/x-ms-asf" ] # MEDIA NOT IMPLEMENTED
|
"hyper",
|
||||||
|
"ipfs",
|
||||||
|
"ipns",
|
||||||
|
"irc",
|
||||||
|
"xmpp",
|
||||||
|
"ircs",
|
||||||
|
"magnet",
|
||||||
|
"mailto",
|
||||||
|
"mumble",
|
||||||
|
"ssb",
|
||||||
|
"gemini",
|
||||||
|
] # NOT IMPLEMENTED
|
||||||
|
allowed_mime_types = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/heic",
|
||||||
|
"image/heif",
|
||||||
|
"image/webp",
|
||||||
|
"image/avif",
|
||||||
|
"video/webm",
|
||||||
|
"video/mp4",
|
||||||
|
"video/quicktime",
|
||||||
|
"video/ogg",
|
||||||
|
"audio/wave",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/x-wav",
|
||||||
|
"audio/x-pn-wave",
|
||||||
|
"audio/vnd.wave",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/vorbis",
|
||||||
|
"audio/mpeg",
|
||||||
|
"audio/mp3",
|
||||||
|
"audio/webm",
|
||||||
|
"audio/flac",
|
||||||
|
"audio/aac",
|
||||||
|
"audio/m4a",
|
||||||
|
"audio/x-m4a",
|
||||||
|
"audio/mp4",
|
||||||
|
"audio/3gpp",
|
||||||
|
"video/x-ms-asf",
|
||||||
|
] # MEDIA NOT IMPLEMENTED
|
||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
# Default visibility for new notes
|
# Default visibility for new notes
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,7 @@ export const createNewStatus = async (data: {
|
||||||
content_type?: string;
|
content_type?: string;
|
||||||
uri?: string;
|
uri?: string;
|
||||||
mentions?: User[];
|
mentions?: User[];
|
||||||
|
media_attachments?: string[];
|
||||||
reply?: {
|
reply?: {
|
||||||
status: Status;
|
status: Status;
|
||||||
user: User;
|
user: User;
|
||||||
|
|
@ -397,6 +398,15 @@ export const createNewStatus = async (data: {
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
attachments: data.media_attachments
|
||||||
|
? {
|
||||||
|
connect: data.media_attachments.map(attachment => {
|
||||||
|
return {
|
||||||
|
id: attachment,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
inReplyToPostId: data.reply?.status.id,
|
inReplyToPostId: data.reply?.status.id,
|
||||||
quotingPostId: data.quote?.id,
|
quotingPostId: data.quote?.id,
|
||||||
instanceId: data.account.instanceId || undefined,
|
instanceId: data.account.instanceId || undefined,
|
||||||
|
|
|
||||||
|
|
@ -78,26 +78,10 @@ export default async (
|
||||||
}>(req);
|
}>(req);
|
||||||
|
|
||||||
// Validate status
|
// Validate status
|
||||||
if (!status) {
|
if (!status && !(media_ids && media_ids.length > 0)) {
|
||||||
return errorResponse("Status is required", 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
`Status must be less than ${config.validation.max_note_size} characters`,
|
"Status is required unless media is attached",
|
||||||
400
|
422
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,14 +99,74 @@ export default async (
|
||||||
return errorResponse("Poll options must be less than 5", 422);
|
return errorResponse("Poll options must be less than 5", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate poll expires_in
|
if (media_ids && media_ids.length > 0) {
|
||||||
if (expires_in && (expires_in < 60 || expires_in > 604800)) {
|
// Disallow poll
|
||||||
|
if (options) {
|
||||||
|
return errorResponse("Cannot attach poll to media", 422);
|
||||||
|
}
|
||||||
|
if (media_ids.length > 4) {
|
||||||
|
return errorResponse("Media IDs must be less than 5", 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && options.length > config.validation.max_poll_options) {
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
"Poll expires_in must be between 60 and 604800",
|
`Poll options must be less than ${config.validation.max_poll_options}`,
|
||||||
422
|
422
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options &&
|
||||||
|
options.some(
|
||||||
|
option => option.length > config.validation.max_poll_option_size
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return errorResponse(
|
||||||
|
`Poll options must be less than ${config.validation.max_poll_option_size} characters`,
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expires_in && expires_in < config.validation.min_poll_duration) {
|
||||||
|
return errorResponse(
|
||||||
|
`Poll duration must be greater than ${config.validation.min_poll_duration} seconds`,
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expires_in && expires_in > config.validation.max_poll_duration) {
|
||||||
|
return errorResponse(
|
||||||
|
`Poll duration must be less than ${config.validation.max_poll_duration} seconds`,
|
||||||
|
422
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduled_at) {
|
||||||
|
if (new Date(scheduled_at).getTime() < Date.now()) {
|
||||||
|
return errorResponse("Scheduled time must be in the future", 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return errorResponse(
|
||||||
|
`Status must be less than ${config.validation.max_note_size} characters`,
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Validate visibility
|
// Validate visibility
|
||||||
if (
|
if (
|
||||||
visibility &&
|
visibility &&
|
||||||
|
|
@ -145,10 +189,24 @@ export default async (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if status body doesnt match filters
|
// Check if status body doesnt match filters
|
||||||
if (config.filters.note_filters.some(filter => status.match(filter))) {
|
if (config.filters.note_filters.some(filter => status?.match(filter))) {
|
||||||
return errorResponse("Status contains blocked words", 422);
|
return errorResponse("Status contains blocked words", 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if media attachments are all valid
|
||||||
|
|
||||||
|
const foundAttachments = await client.attachment.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: media_ids ?? [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||||
|
return errorResponse("Invalid media IDs", 422);
|
||||||
|
}
|
||||||
|
|
||||||
const newStatus = await createNewStatus({
|
const newStatus = await createNewStatus({
|
||||||
account: user,
|
account: user,
|
||||||
application,
|
application,
|
||||||
|
|
@ -163,6 +221,7 @@ export default async (
|
||||||
sensitive: sensitive || false,
|
sensitive: sensitive || false,
|
||||||
spoiler_text: spoiler_text || "",
|
spoiler_text: spoiler_text || "",
|
||||||
emojis: [],
|
emojis: [],
|
||||||
|
media_attachments: media_ids,
|
||||||
reply:
|
reply:
|
||||||
replyStatus && replyUser
|
replyStatus && replyUser
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
const file = form.get("file") as unknown as File | undefined;
|
const file = form.get("file") as unknown as File | undefined;
|
||||||
const thumbnail = form.get("thumbnail");
|
const thumbnail = form.get("thumbnail");
|
||||||
const description = form.get("description");
|
const description = form.get("description") as string | undefined;
|
||||||
|
|
||||||
// Floating point numbers from -1.0 to 1.0, comma delimited
|
// Floating point numbers from -1.0 to 1.0, comma delimited
|
||||||
// const focus = form.get("focus");
|
// const focus = form.get("focus");
|
||||||
|
|
@ -45,6 +45,29 @@ export default async (req: Request): Promise<Response> => {
|
||||||
return errorResponse("No file provided", 400);
|
return errorResponse("No file provided", 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
if (file.size > config.validation.max_media_size) {
|
||||||
|
return errorResponse(
|
||||||
|
`File too large, max size is ${config.validation.max_media_size} bytes`,
|
||||||
|
413
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.validation.allowed_mime_types.includes(file.type)) {
|
||||||
|
return errorResponse("Invalid file type", 415);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
description &&
|
||||||
|
description.length > config.validation.max_media_description_size
|
||||||
|
) {
|
||||||
|
return errorResponse(
|
||||||
|
`Description too long, max length is ${config.validation.max_media_description_size} characters`,
|
||||||
|
413
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sha256 = new Bun.SHA256();
|
const sha256 = new Bun.SHA256();
|
||||||
|
|
||||||
const isImage = file.type.startsWith("image/");
|
const isImage = file.type.startsWith("image/");
|
||||||
|
|
@ -65,8 +88,6 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
let url = "";
|
let url = "";
|
||||||
|
|
||||||
const config = getConfig();
|
|
||||||
|
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
const hash = await uploadFile(file, config);
|
const hash = await uploadFile(file, config);
|
||||||
|
|
||||||
|
|
@ -87,7 +108,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
thumbnail_url: thumbnailUrl,
|
thumbnail_url: thumbnailUrl,
|
||||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||||
mime_type: file.type,
|
mime_type: file.type,
|
||||||
description: (description as string | undefined) ?? "",
|
description: description ?? "",
|
||||||
size: file.size,
|
size: file.size,
|
||||||
blurhash: blurhash ?? undefined,
|
blurhash: blurhash ?? undefined,
|
||||||
width: metadata?.width ?? undefined,
|
width: metadata?.width ?? undefined,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ export interface ConfigType {
|
||||||
max_media_size: number;
|
max_media_size: number;
|
||||||
max_media_attachments: number;
|
max_media_attachments: number;
|
||||||
max_media_description_size: number;
|
max_media_description_size: number;
|
||||||
|
max_poll_options: number;
|
||||||
|
max_poll_option_size: number;
|
||||||
|
min_poll_duration: number;
|
||||||
|
max_poll_duration: number;
|
||||||
|
|
||||||
username_blacklist: string[];
|
username_blacklist: string[];
|
||||||
blacklist_tempmail: boolean;
|
blacklist_tempmail: boolean;
|
||||||
|
|
@ -181,8 +185,12 @@ export const configDefaults: ConfigType = {
|
||||||
max_avatar_size: 5_000_000,
|
max_avatar_size: 5_000_000,
|
||||||
max_header_size: 5_000_000,
|
max_header_size: 5_000_000,
|
||||||
max_media_size: 40_000_000,
|
max_media_size: 40_000_000,
|
||||||
max_media_attachments: 4,
|
max_media_attachments: 10,
|
||||||
max_media_description_size: 1000,
|
max_media_description_size: 1000,
|
||||||
|
max_poll_options: 20,
|
||||||
|
max_poll_option_size: 500,
|
||||||
|
min_poll_duration: 60,
|
||||||
|
max_poll_duration: 1893456000,
|
||||||
max_username_size: 30,
|
max_username_size: 30,
|
||||||
|
|
||||||
username_blacklist: [
|
username_blacklist: [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue