mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Add status pinning and unpinning, fix bugs
This commit is contained in:
parent
0a74bbfe93
commit
f51476e810
11
README.md
11
README.md
|
|
@ -144,6 +144,8 @@ Working endpoints are:
|
||||||
- `/api/v1/statuses/:id/reblogged_by`
|
- `/api/v1/statuses/:id/reblogged_by`
|
||||||
- `/api/v1/statuses/:id/reblog`
|
- `/api/v1/statuses/:id/reblog`
|
||||||
- `/api/v1/statuses/:id/unreblog`
|
- `/api/v1/statuses/:id/unreblog`
|
||||||
|
- `/api/v1/statuses/:id/pin`
|
||||||
|
- `/api/v1/statuses/:id/unpin`
|
||||||
- `/api/v1/statuses`
|
- `/api/v1/statuses`
|
||||||
- `/api/v1/timelines/public`
|
- `/api/v1/timelines/public`
|
||||||
- `/api/v1/timelines/home`
|
- `/api/v1/timelines/home`
|
||||||
|
|
@ -153,14 +155,13 @@ Working endpoints are:
|
||||||
- `/api/v1/apps/verify_credentials`
|
- `/api/v1/apps/verify_credentials`
|
||||||
- `/oauth/authorize`
|
- `/oauth/authorize`
|
||||||
- `/oauth/token`
|
- `/oauth/token`
|
||||||
|
- `/api/v1/blocks`
|
||||||
|
- `/api/v1/mutes`
|
||||||
|
- `/api/v2/media`
|
||||||
|
|
||||||
Tests needed but completed:
|
Tests needed but completed:
|
||||||
|
|
||||||
- `/api/v2/media`
|
|
||||||
- `/api/v1/media/:id`
|
- `/api/v1/media/:id`
|
||||||
- `/api/v1/blocks`
|
|
||||||
- `/api/v1/mutes`
|
|
||||||
|
|
||||||
|
|
||||||
Endpoints left:
|
Endpoints left:
|
||||||
|
|
||||||
|
|
@ -195,8 +196,6 @@ Endpoints left:
|
||||||
- `/api/v1/statuses/:id/unbookmark`
|
- `/api/v1/statuses/:id/unbookmark`
|
||||||
- `/api/v1/statuses/:id/mute`
|
- `/api/v1/statuses/:id/mute`
|
||||||
- `/api/v1/statuses/:id/unmute`
|
- `/api/v1/statuses/:id/unmute`
|
||||||
- `/api/v1/statuses/:id/pin`
|
|
||||||
- `/api/v1/statuses/:id/unpin`
|
|
||||||
- `/api/v1/statuses/:id` (`PUT`)
|
- `/api/v1/statuses/:id` (`PUT`)
|
||||||
- `/api/v1/statuses/:id/history`
|
- `/api/v1/statuses/:id/history`
|
||||||
- `/api/v1/statuses/:id/source`
|
- `/api/v1/statuses/:id/source`
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,8 @@ url_scheme_whitelist = [
|
||||||
"ssb",
|
"ssb",
|
||||||
"gemini",
|
"gemini",
|
||||||
] # NOT IMPLEMENTED
|
] # NOT IMPLEMENTED
|
||||||
|
|
||||||
|
enforce_mime_types = false
|
||||||
allowed_mime_types = [
|
allowed_mime_types = [
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
"image/png",
|
"image/png",
|
||||||
|
|
@ -152,7 +154,7 @@ allowed_mime_types = [
|
||||||
"audio/mp4",
|
"audio/mp4",
|
||||||
"audio/3gpp",
|
"audio/3gpp",
|
||||||
"video/x-ms-asf",
|
"video/x-ms-asf",
|
||||||
] # MEDIA NOT IMPLEMENTED
|
]
|
||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
# Default visibility for new notes
|
# Default visibility for new notes
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,13 @@ import { client } from "~database/datasource";
|
||||||
import type { LysandPublication, Note } from "~types/lysand/Object";
|
import type { LysandPublication, Note } from "~types/lysand/Object";
|
||||||
import { htmlToText } from "html-to-text";
|
import { htmlToText } from "html-to-text";
|
||||||
import { getBestContentType } from "@content_types";
|
import { getBestContentType } from "@content_types";
|
||||||
import type {
|
import {
|
||||||
Application,
|
Prisma,
|
||||||
Emoji,
|
type Application,
|
||||||
Instance,
|
type Emoji,
|
||||||
Like,
|
type Relationship,
|
||||||
Relationship,
|
type Status,
|
||||||
Status,
|
type User,
|
||||||
User,
|
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
||||||
import type { APIStatus } from "~types/entities/status";
|
import type { APIStatus } from "~types/entities/status";
|
||||||
|
|
@ -26,7 +25,7 @@ import { applicationToAPI } from "./Application";
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
export const statusAndUserRelations = {
|
export const statusAndUserRelations: Prisma.StatusInclude = {
|
||||||
author: {
|
author: {
|
||||||
include: userRelations,
|
include: userRelations,
|
||||||
},
|
},
|
||||||
|
|
@ -54,6 +53,7 @@ export const statusAndUserRelations = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
attachments: true,
|
||||||
instance: true,
|
instance: true,
|
||||||
mentions: true,
|
mentions: true,
|
||||||
pinnedBy: true,
|
pinnedBy: true,
|
||||||
|
|
@ -115,64 +115,13 @@ export const statusAndUserRelations = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatusWithRelations = Status & {
|
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
|
||||||
author: UserWithRelations;
|
include: statusAndUserRelations,
|
||||||
application: Application | null;
|
});
|
||||||
emojis: Emoji[];
|
|
||||||
inReplyToPost:
|
export type StatusWithRelations = Prisma.StatusGetPayload<
|
||||||
| (Status & {
|
typeof statusRelations
|
||||||
author: UserWithRelations;
|
>;
|
||||||
application: Application | null;
|
|
||||||
emojis: Emoji[];
|
|
||||||
inReplyToPost: Status | null;
|
|
||||||
instance: Instance | null;
|
|
||||||
mentions: User[];
|
|
||||||
pinnedBy: User[];
|
|
||||||
_count: {
|
|
||||||
replies: number;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
| null;
|
|
||||||
instance: Instance | null;
|
|
||||||
mentions: User[];
|
|
||||||
pinnedBy: User[];
|
|
||||||
_count: {
|
|
||||||
replies: number;
|
|
||||||
likes: number;
|
|
||||||
reblogs: number;
|
|
||||||
};
|
|
||||||
reblog:
|
|
||||||
| (Status & {
|
|
||||||
author: UserWithRelations;
|
|
||||||
application: Application | null;
|
|
||||||
emojis: Emoji[];
|
|
||||||
inReplyToPost: Status | null;
|
|
||||||
instance: Instance | null;
|
|
||||||
mentions: User[];
|
|
||||||
pinnedBy: User[];
|
|
||||||
_count: {
|
|
||||||
replies: number;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
| null;
|
|
||||||
quotingPost:
|
|
||||||
| (Status & {
|
|
||||||
author: UserWithRelations;
|
|
||||||
application: Application | null;
|
|
||||||
emojis: Emoji[];
|
|
||||||
inReplyToPost: Status | null;
|
|
||||||
instance: Instance | null;
|
|
||||||
mentions: User[];
|
|
||||||
pinnedBy: User[];
|
|
||||||
_count: {
|
|
||||||
replies: number;
|
|
||||||
};
|
|
||||||
})
|
|
||||||
| null;
|
|
||||||
likes: (Like & {
|
|
||||||
liker: User;
|
|
||||||
})[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a status (i.e. a post)
|
* Represents a status (i.e. a post)
|
||||||
|
|
@ -494,7 +443,7 @@ export const statusToAPI = async (
|
||||||
? user.relationships.find(r => r.subjectId == status.authorId)
|
? user.relationships.find(r => r.subjectId == status.authorId)
|
||||||
?.muting || false
|
?.muting || false
|
||||||
: false,
|
: false,
|
||||||
pinned: status.author.pinnedNotes.some(note => note.id === status.id),
|
pinned: status.pinnedBy.find(u => u.id === user?.id) ? true : false,
|
||||||
// TODO: Add pols
|
// TODO: Add pols
|
||||||
poll: null,
|
poll: null,
|
||||||
reblog: status.reblog
|
reblog: status.reblog
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,8 @@ import { getConfig } from "@config";
|
||||||
import type { APIAccount } from "~types/entities/account";
|
import type { APIAccount } from "~types/entities/account";
|
||||||
import type { User as LysandUser } from "~types/lysand/Object";
|
import type { User as LysandUser } from "~types/lysand/Object";
|
||||||
import { htmlToText } from "html-to-text";
|
import { htmlToText } from "html-to-text";
|
||||||
import type {
|
import type { User } from "@prisma/client";
|
||||||
Emoji,
|
import { Prisma } from "@prisma/client";
|
||||||
Instance,
|
|
||||||
Like,
|
|
||||||
Relationship,
|
|
||||||
Status,
|
|
||||||
User,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import { client } from "~database/datasource";
|
import { client } from "~database/datasource";
|
||||||
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
|
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
|
||||||
import { addInstanceIfNotExists } from "./Instance";
|
import { addInstanceIfNotExists } from "./Instance";
|
||||||
|
|
@ -26,7 +20,7 @@ export interface AuthData {
|
||||||
* Stores local and remote users
|
* Stores local and remote users
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const userRelations = {
|
export const userRelations: Prisma.UserInclude = {
|
||||||
emojis: true,
|
emojis: true,
|
||||||
instance: true,
|
instance: true,
|
||||||
likes: true,
|
likes: true,
|
||||||
|
|
@ -41,18 +35,11 @@ export const userRelations = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserWithRelations = User & {
|
const userRelations2 = Prisma.validator<Prisma.UserDefaultArgs>()({
|
||||||
emojis: Emoji[];
|
include: userRelations,
|
||||||
instance: Instance | null;
|
});
|
||||||
likes: Like[];
|
|
||||||
relationships: Relationship[];
|
export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
||||||
relationshipSubjects: Relationship[];
|
|
||||||
pinnedNotes: Status[];
|
|
||||||
_count: {
|
|
||||||
statuses: number;
|
|
||||||
likes: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user's avatar in raw URL format
|
* Get the user's avatar in raw URL format
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,14 @@ export default async (
|
||||||
max_id,
|
max_id,
|
||||||
min_id,
|
min_id,
|
||||||
since_id,
|
since_id,
|
||||||
limit,
|
limit = "20",
|
||||||
exclude_reblogs,
|
exclude_reblogs,
|
||||||
|
pinned,
|
||||||
}: {
|
}: {
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
since_id?: string;
|
since_id?: string;
|
||||||
min_id?: string;
|
min_id?: string;
|
||||||
limit?: number;
|
limit?: string;
|
||||||
only_media?: boolean;
|
only_media?: boolean;
|
||||||
exclude_replies?: boolean;
|
exclude_replies?: boolean;
|
||||||
exclude_reblogs?: boolean;
|
exclude_reblogs?: boolean;
|
||||||
|
|
@ -54,6 +55,48 @@ export default async (
|
||||||
|
|
||||||
if (!user) return errorResponse("User not found", 404);
|
if (!user) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
|
if (pinned) {
|
||||||
|
const objects = await client.status.findMany({
|
||||||
|
where: {
|
||||||
|
authorId: id,
|
||||||
|
isReblog: false,
|
||||||
|
pinnedBy: {
|
||||||
|
some: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
lt: max_id,
|
||||||
|
gt: min_id,
|
||||||
|
gte: since_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: statusAndUserRelations,
|
||||||
|
take: Number(limit),
|
||||||
|
orderBy: {
|
||||||
|
id: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Constuct HTTP Link header (next and prev)
|
||||||
|
const linkHeader = [];
|
||||||
|
if (objects.length > 0) {
|
||||||
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
|
linkHeader.push(
|
||||||
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse(
|
||||||
|
await Promise.all(objects.map(status => statusToAPI(status, user))),
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
Link: linkHeader.join(", "),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const objects = await client.status.findMany({
|
const objects = await client.status.findMany({
|
||||||
where: {
|
where: {
|
||||||
authorId: id,
|
authorId: id,
|
||||||
|
|
@ -65,7 +108,7 @@ export default async (
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
take: limit ?? 20,
|
take: Number(limit),
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: "desc",
|
id: "desc",
|
||||||
},
|
},
|
||||||
|
|
@ -76,11 +119,8 @@ export default async (
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
);
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||||
linkHeader.push(
|
|
||||||
`<${urlWithoutQuery}?since_id=${objects.at(-1)
|
|
||||||
?.id}&limit=${limit}>; rel="prev"`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
const blocks = await client.user.findMany({
|
const blocks = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationshipSubjects: {
|
relationshipSubjects: {
|
||||||
every: {
|
some: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
const blocks = await client.user.findMany({
|
const blocks = await client.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
relationshipSubjects: {
|
relationshipSubjects: {
|
||||||
every: {
|
some: {
|
||||||
ownerId: user.id,
|
ownerId: user.id,
|
||||||
muting: true,
|
muting: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
65
server/api/api/v1/statuses/[id]/pin.ts
Normal file
65
server/api/api/v1/statuses/[id]/pin.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import { applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import type { MatchedRoute } from "bun";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
|
||||||
|
import { getFromRequest } from "~database/entities/User";
|
||||||
|
import type { APIRouteMeta } from "~types/api";
|
||||||
|
|
||||||
|
export const meta: APIRouteMeta = applyConfig({
|
||||||
|
allowedMethods: ["POST"],
|
||||||
|
ratelimits: {
|
||||||
|
max: 100,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
route: "/api/v1/statuses/:id/pin",
|
||||||
|
auth: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin a post
|
||||||
|
*/
|
||||||
|
export default async (
|
||||||
|
req: Request,
|
||||||
|
matchedRoute: MatchedRoute
|
||||||
|
): Promise<Response> => {
|
||||||
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
|
const { user } = await getFromRequest(req);
|
||||||
|
|
||||||
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
let status = await client.status.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: statusAndUserRelations,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if status exists
|
||||||
|
if (!status) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
// Check if status is user's
|
||||||
|
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
await client.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
pinnedNotes: {
|
||||||
|
connect: {
|
||||||
|
id: status.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
status = await client.status.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: statusAndUserRelations,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
return jsonResponse(statusToAPI(status, user));
|
||||||
|
};
|
||||||
65
server/api/api/v1/statuses/[id]/unpin.ts
Normal file
65
server/api/api/v1/statuses/[id]/unpin.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import { applyConfig } from "@api";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import type { MatchedRoute } from "bun";
|
||||||
|
import { client } from "~database/datasource";
|
||||||
|
import { statusAndUserRelations, statusToAPI } from "~database/entities/Status";
|
||||||
|
import { getFromRequest } from "~database/entities/User";
|
||||||
|
import type { APIRouteMeta } from "~types/api";
|
||||||
|
|
||||||
|
export const meta: APIRouteMeta = applyConfig({
|
||||||
|
allowedMethods: ["POST"],
|
||||||
|
ratelimits: {
|
||||||
|
max: 100,
|
||||||
|
duration: 60,
|
||||||
|
},
|
||||||
|
route: "/api/v1/statuses/:id/unpin",
|
||||||
|
auth: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpins a post
|
||||||
|
*/
|
||||||
|
export default async (
|
||||||
|
req: Request,
|
||||||
|
matchedRoute: MatchedRoute
|
||||||
|
): Promise<Response> => {
|
||||||
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
|
const { user } = await getFromRequest(req);
|
||||||
|
|
||||||
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
let status = await client.status.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: statusAndUserRelations,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if status exists
|
||||||
|
if (!status) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
// Check if status is user's
|
||||||
|
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
|
await client.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
pinnedNotes: {
|
||||||
|
disconnect: {
|
||||||
|
id: status.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
status = await client.status.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: statusAndUserRelations,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
return jsonResponse(statusToAPI(status, user));
|
||||||
|
};
|
||||||
|
|
@ -58,8 +58,8 @@ export default async (req: Request): Promise<Response> => {
|
||||||
not: null,
|
not: null,
|
||||||
}
|
}
|
||||||
: local
|
: local
|
||||||
? null
|
? null
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
include: statusAndUserRelations,
|
include: statusAndUserRelations,
|
||||||
take: limit,
|
take: limit,
|
||||||
|
|
@ -73,12 +73,8 @@ export default async (req: Request): Promise<Response> => {
|
||||||
if (objects.length > 0) {
|
if (objects.length > 0) {
|
||||||
const urlWithoutQuery = req.url.split("?")[0];
|
const urlWithoutQuery = req.url.split("?")[0];
|
||||||
linkHeader.push(
|
linkHeader.push(
|
||||||
`<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`
|
`<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`,
|
||||||
);
|
`<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`
|
||||||
linkHeader.push(
|
|
||||||
`<${urlWithoutQuery}?since_id=${
|
|
||||||
objects[objects.length - 1].id
|
|
||||||
}&limit=${limit}>; rel="prev"`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,10 @@ export default async (req: Request): Promise<Response> => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.validation.allowed_mime_types.includes(file.type)) {
|
if (
|
||||||
|
config.validation.enforce_mime_types &&
|
||||||
|
!config.validation.allowed_mime_types.includes(file.type)
|
||||||
|
) {
|
||||||
return errorResponse("Invalid file type", 415);
|
return errorResponse("Invalid file type", 415);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,14 @@ let user2: UserWithRelations;
|
||||||
|
|
||||||
describe("API Tests", () => {
|
describe("API Tests", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
await client.user.deleteMany({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
in: ["test", "test2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
user = await createNewLocalUser({
|
user = await createNewLocalUser({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
username: "test",
|
username: "test",
|
||||||
|
|
@ -291,6 +299,30 @@ describe("API Tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("GET /api/v1/blocks", () => {
|
||||||
|
test("should return an array of APIAccount objects for the user's blocked accounts", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}/api/v1/blocks`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe(
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
const body = (await response.json()) as APIAccount[];
|
||||||
|
|
||||||
|
expect(Array.isArray(body)).toBe(true);
|
||||||
|
expect(body.length).toBe(1);
|
||||||
|
expect(body[0].id).toBe(user2.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("POST /api/v1/accounts/:id/unblock", () => {
|
describe("POST /api/v1/accounts/:id/unblock", () => {
|
||||||
test("should unblock the specified user and return an APIRelationship object", async () => {
|
test("should unblock the specified user and return an APIRelationship object", async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
@ -369,6 +401,31 @@ describe("API Tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("GET /api/v1/mutes", () => {
|
||||||
|
test("should return an array of APIAccount objects for the user's muted accounts", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}/api/v1/mutes`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe(
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = (await response.json()) as APIAccount[];
|
||||||
|
|
||||||
|
expect(Array.isArray(body)).toBe(true);
|
||||||
|
expect(body.length).toBe(1);
|
||||||
|
expect(body[0].id).toBe(user2.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("POST /api/v1/accounts/:id/unmute", () => {
|
describe("POST /api/v1/accounts/:id/unmute", () => {
|
||||||
test("should unmute the specified user and return an APIRelationship object", async () => {
|
test("should unmute the specified user and return an APIRelationship object", async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
createNewLocalUser,
|
createNewLocalUser,
|
||||||
} from "~database/entities/User";
|
} from "~database/entities/User";
|
||||||
import type { APIAccount } from "~types/entities/account";
|
import type { APIAccount } from "~types/entities/account";
|
||||||
|
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
|
||||||
import type { APIContext } from "~types/entities/context";
|
import type { APIContext } from "~types/entities/context";
|
||||||
import type { APIStatus } from "~types/entities/status";
|
import type { APIStatus } from "~types/entities/status";
|
||||||
|
|
||||||
|
|
@ -19,9 +20,18 @@ let token: Token;
|
||||||
let user: UserWithRelations;
|
let user: UserWithRelations;
|
||||||
let status: APIStatus | null = null;
|
let status: APIStatus | null = null;
|
||||||
let status2: APIStatus | null = null;
|
let status2: APIStatus | null = null;
|
||||||
|
let media1: APIAsyncAttachment | null = null;
|
||||||
|
|
||||||
describe("API Tests", () => {
|
describe("API Tests", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
await client.user.deleteMany({
|
||||||
|
where: {
|
||||||
|
username: {
|
||||||
|
in: ["test", "test2"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
user = await createNewLocalUser({
|
user = await createNewLocalUser({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
username: "test",
|
username: "test",
|
||||||
|
|
@ -65,6 +75,36 @@ describe("API Tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("POST /api/v2/media", () => {
|
||||||
|
test("should upload a file and return a MediaAttachment object", async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", new Blob(["test"], { type: "text/plain" }));
|
||||||
|
|
||||||
|
// @ts-expect-error FormData is not iterable
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}/api/v2/media`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token.access_token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(202);
|
||||||
|
expect(response.headers.get("content-type")).toBe(
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
media1 = (await response.json()) as APIAsyncAttachment;
|
||||||
|
|
||||||
|
expect(media1.id).toBeDefined();
|
||||||
|
expect(media1.type).toBe("unknown");
|
||||||
|
expect(media1.url).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("POST /api/v1/statuses", () => {
|
describe("POST /api/v1/statuses", () => {
|
||||||
test("should create a new status and return an APIStatus object", async () => {
|
test("should create a new status and return an APIStatus object", async () => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
@ -78,6 +118,7 @@ describe("API Tests", () => {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
status: "Hello, world!",
|
status: "Hello, world!",
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
|
media_ids: [media1?.id],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -327,7 +368,7 @@ describe("API Tests", () => {
|
||||||
|
|
||||||
expect(statuses.length).toBe(2);
|
expect(statuses.length).toBe(2);
|
||||||
|
|
||||||
const status1 = statuses[1];
|
const status1 = statuses[0];
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
expect(status1.content).toBe("This is a reply!");
|
expect(status1.content).toBe("This is a reply!");
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export interface ConfigType {
|
||||||
email_blacklist: string[];
|
email_blacklist: string[];
|
||||||
url_scheme_whitelist: string[];
|
url_scheme_whitelist: string[];
|
||||||
|
|
||||||
|
enforce_mime_types: boolean;
|
||||||
allowed_mime_types: string[];
|
allowed_mime_types: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -245,6 +246,7 @@ export const configDefaults: ConfigType = {
|
||||||
"ssb",
|
"ssb",
|
||||||
],
|
],
|
||||||
|
|
||||||
|
enforce_mime_types: false,
|
||||||
allowed_mime_types: [],
|
allowed_mime_types: [],
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue