Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { argsToBools, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { UserConverter } from "../converters/user.js";
|
||||
import { NoteConverter } from "../converters/note.js";
|
||||
import { UserHelpers } from "../helpers/user.js";
|
||||
import { ListHelpers } from "../helpers/list.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { SearchHelpers } from "../helpers/search.js";
|
||||
import { filterContext } from "../middleware/filter-context.js";
|
||||
export function setupEndpointsAccount(router) {
|
||||
router.get("/v1/accounts/verify_credentials", auth(true, [
|
||||
'read:accounts'
|
||||
]), async (ctx)=>{
|
||||
ctx.body = await UserHelpers.verifyCredentials(ctx);
|
||||
});
|
||||
router.patch("/v1/accounts/update_credentials", auth(true, [
|
||||
'write:accounts'
|
||||
]), async (ctx)=>{
|
||||
ctx.body = await UserHelpers.updateCredentials(ctx);
|
||||
});
|
||||
router.get("/v1/accounts/lookup", async (ctx)=>{
|
||||
const args = normalizeUrlQuery(ctx.query);
|
||||
const user = await UserHelpers.getUserFromAcct(args.acct);
|
||||
ctx.body = await UserConverter.encode(user, ctx);
|
||||
});
|
||||
router.get("/v1/accounts/relationships", auth(true, [
|
||||
'read:follows'
|
||||
]), async (ctx)=>{
|
||||
const ids = normalizeUrlQuery(ctx.query, [
|
||||
'id[]'
|
||||
])['id[]'] ?? normalizeUrlQuery(ctx.query, [
|
||||
'id'
|
||||
])['id'] ?? [];
|
||||
ctx.body = await UserHelpers.getUserRelationhipToMany(ids, ctx.user.id);
|
||||
});
|
||||
// This must come before /accounts/:id, otherwise that will take precedence
|
||||
router.get("/v1/accounts/search", auth(true, [
|
||||
'read:accounts'
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query), [
|
||||
'resolve',
|
||||
'following'
|
||||
]));
|
||||
ctx.body = await SearchHelpers.search(args.q, 'accounts', args.resolve, args.following, undefined, false, undefined, undefined, args.limit, args.offset, ctx).then((p)=>p.accounts);
|
||||
});
|
||||
router.get("/v1/accounts/:id", auth(false), async (ctx)=>{
|
||||
ctx.body = await UserConverter.encode(await UserHelpers.getUserOr404(ctx.params.id), ctx);
|
||||
});
|
||||
router.get("/v1/accounts/:id/statuses", auth(false, [
|
||||
"read:statuses"
|
||||
]), filterContext('account'), async (ctx)=>{
|
||||
const query = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)));
|
||||
const res = await UserHelpers.getUserStatuses(query, args.max_id, args.since_id, args.min_id, args.limit, args['only_media'], args['exclude_replies'], args['exclude_reblogs'], args.pinned, args.tagged, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/accounts/:id/featured_tags", async (ctx)=>{
|
||||
ctx.body = [];
|
||||
});
|
||||
router.get("/v1/accounts/:id/followers", auth(false), async (ctx)=>{
|
||||
const query = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await UserHelpers.getUserFollowers(query, args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/accounts/:id/following", auth(false), async (ctx)=>{
|
||||
const query = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await UserHelpers.getUserFollowing(query, args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/accounts/:id/lists", auth(true, [
|
||||
"read:lists"
|
||||
]), async (ctx)=>{
|
||||
const member = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await ListHelpers.getListsByMember(member, ctx);
|
||||
});
|
||||
router.post("/v1/accounts/:id/follow", auth(true, [
|
||||
"write:follows"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
//FIXME: Parse form data
|
||||
ctx.body = await UserHelpers.followUser(target, true, false, ctx);
|
||||
});
|
||||
router.post("/v1/accounts/:id/unfollow", auth(true, [
|
||||
"write:follows"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.unfollowUser(target, ctx);
|
||||
});
|
||||
router.post("/v1/accounts/:id/block", auth(true, [
|
||||
"write:blocks"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.blockUser(target, ctx);
|
||||
});
|
||||
router.post("/v1/accounts/:id/unblock", auth(true, [
|
||||
"write:blocks"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.unblockUser(target, ctx);
|
||||
});
|
||||
router.post("/v1/accounts/:id/mute", auth(true, [
|
||||
"write:mutes"
|
||||
]), async (ctx)=>{
|
||||
//FIXME: parse form data
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query, [
|
||||
'duration'
|
||||
]), [
|
||||
'notifications'
|
||||
]));
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.muteUser(target, args.notifications, args.duration, ctx);
|
||||
});
|
||||
router.post("/v1/accounts/:id/unmute", auth(true, [
|
||||
"write:mutes"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.unmuteUser(target, ctx);
|
||||
});
|
||||
router.get("/v1/featured_tags", async (ctx)=>{
|
||||
ctx.body = [];
|
||||
});
|
||||
router.get("/v1/followed_tags", async (ctx)=>{
|
||||
ctx.body = [];
|
||||
});
|
||||
router.get("/v1/bookmarks", auth(true, [
|
||||
"read:bookmarks"
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await UserHelpers.getUserBookmarks(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/favourites", auth(true, [
|
||||
"read:favourites"
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await UserHelpers.getUserFavorites(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/mutes", auth(true, [
|
||||
"read:mutes"
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
ctx.body = await UserHelpers.getUserMutes(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
});
|
||||
router.get("/v1/blocks", auth(true, [
|
||||
"read:blocks"
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await UserHelpers.getUserBlocks(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/follow_requests", auth(true, [
|
||||
"read:follows"
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await UserHelpers.getUserFollowRequests(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.post("/v1/follow_requests/:id/authorize", auth(true, [
|
||||
"write:follows"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.acceptFollowRequest(target, ctx);
|
||||
});
|
||||
router.post("/v1/follow_requests/:id/reject", auth(true, [
|
||||
"write:follows"
|
||||
]), async (ctx)=>{
|
||||
const target = await UserHelpers.getUserCachedOr404(ctx.params.id, ctx);
|
||||
ctx.body = await UserHelpers.rejectFollowRequest(target, ctx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { AuthHelpers } from "../helpers/auth.js";
|
||||
import { MiAuth } from "../middleware/auth.js";
|
||||
export function setupEndpointsAuth(router) {
|
||||
router.post("/v1/apps", async (ctx)=>{
|
||||
ctx.body = await AuthHelpers.registerApp(ctx);
|
||||
});
|
||||
router.get("/v1/apps/verify_credentials", async (ctx)=>{
|
||||
ctx.body = await AuthHelpers.verifyAppCredentials(ctx);
|
||||
});
|
||||
router.post("/v1/iceshrimp/apps/info", MiAuth(true), async (ctx)=>{
|
||||
ctx.body = await AuthHelpers.getAppInfo(ctx);
|
||||
});
|
||||
router.post("/v1/iceshrimp/auth/code", MiAuth(true), async (ctx)=>{
|
||||
ctx.body = await AuthHelpers.getAuthCode(ctx);
|
||||
});
|
||||
}
|
||||
export function setupEndpointsAuthRoot(router) {
|
||||
router.post("/oauth/token", async (ctx)=>{
|
||||
ctx.body = await AuthHelpers.getAuthToken(ctx);
|
||||
});
|
||||
router.post("/oauth/revoke", async (ctx)=>{
|
||||
ctx.body = await AuthHelpers.revokeAuthToken(ctx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { MastoApiError } from "../middleware/catch-errors.js";
|
||||
export function setupEndpointsFilter(router) {
|
||||
router.get([
|
||||
"/v1/filters",
|
||||
"/v2/filters"
|
||||
], auth(true, [
|
||||
'read:filters'
|
||||
]), async (ctx)=>{
|
||||
ctx.body = [];
|
||||
});
|
||||
router.post([
|
||||
"/v1/filters",
|
||||
"/v2/filters"
|
||||
], auth(true, [
|
||||
'write:filters'
|
||||
]), async (ctx)=>{
|
||||
throw new MastoApiError(400, "Please change word mute settings in the web frontend settings.");
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { ListHelpers } from "../helpers/list.js";
|
||||
import { UserConverter } from "../converters/user.js";
|
||||
import { UserLists } from "../../../../models/index.js";
|
||||
import { getUser } from "../../common/getters.js";
|
||||
import { toArray } from "../../../../prelude/array.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { MastoApiError } from "../middleware/catch-errors.js";
|
||||
export function setupEndpointsList(router) {
|
||||
router.get("/v1/lists", auth(true, [
|
||||
'read:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
ctx.body = await ListHelpers.getLists(ctx);
|
||||
});
|
||||
router.get("/v1/lists/:id", auth(true, [
|
||||
'read:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
ctx.body = await ListHelpers.getListOr404(ctx.params.id, ctx);
|
||||
});
|
||||
router.post("/v1/lists", auth(true, [
|
||||
'write:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
const body = ctx.request.body;
|
||||
const title = (body.title ?? '').trim();
|
||||
ctx.body = await ListHelpers.createList(title, ctx);
|
||||
});
|
||||
router.put("/v1/lists/:id", auth(true, [
|
||||
'write:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
const list = await UserLists.findOneBy({
|
||||
userId: ctx.user.id,
|
||||
id: ctx.params.id
|
||||
});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
const body = ctx.request.body;
|
||||
const title = (body.title ?? '').trim();
|
||||
const exclusive = body.exclusive ?? undefined;
|
||||
ctx.body = await ListHelpers.updateList(list, title, exclusive, ctx);
|
||||
});
|
||||
router.delete("/v1/lists/:id", auth(true, [
|
||||
'write:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
const list = await UserLists.findOneBy({
|
||||
userId: ctx.user.id,
|
||||
id: ctx.params.id
|
||||
});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
await ListHelpers.deleteList(list, ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
router.get("/v1/lists/:id/accounts", auth(true, [
|
||||
'read:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await ListHelpers.getListUsers(ctx.params.id, args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.post("/v1/lists/:id/accounts", auth(true, [
|
||||
'write:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
const list = await UserLists.findOneBy({
|
||||
userId: ctx.user.id,
|
||||
id: ctx.params.id
|
||||
});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
const body = ctx.request.body;
|
||||
if (!body['account_ids']) throw new MastoApiError(400, "Missing account_ids[] field");
|
||||
const ids = toArray(body['account_ids']);
|
||||
const targets = await Promise.all(ids.map((p)=>getUser(p)));
|
||||
await ListHelpers.addToList(list, targets, ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
router.delete("/v1/lists/:id/accounts", auth(true, [
|
||||
'write:lists'
|
||||
]), async (ctx, reply)=>{
|
||||
const list = await UserLists.findOneBy({
|
||||
userId: ctx.user.id,
|
||||
id: ctx.params.id
|
||||
});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
const body = ctx.request.body;
|
||||
if (!body['account_ids']) throw new MastoApiError(400, "Missing account_ids[] field");
|
||||
const ids = toArray(body['account_ids']);
|
||||
const targets = await Promise.all(ids.map((p)=>getUser(p)));
|
||||
await ListHelpers.removeFromList(list, targets, ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MediaHelpers } from "../helpers/media.js";
|
||||
import { FileConverter } from "../converters/file.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
export function setupEndpointsMedia(router) {
|
||||
router.get("/v1/media/:id", auth(true, [
|
||||
'write:media'
|
||||
]), async (ctx)=>{
|
||||
const file = await MediaHelpers.getMediaPackedOr404(ctx.params.id, ctx);
|
||||
ctx.body = FileConverter.encode(file);
|
||||
});
|
||||
router.put("/v1/media/:id", auth(true, [
|
||||
'write:media'
|
||||
]), async (ctx)=>{
|
||||
const file = await MediaHelpers.getMediaOr404(ctx.params.id, ctx);
|
||||
ctx.body = await MediaHelpers.updateMedia(file, ctx).then((p)=>FileConverter.encode(p));
|
||||
});
|
||||
router.post([
|
||||
"/v2/media",
|
||||
"/v1/media"
|
||||
], auth(true, [
|
||||
'write:media'
|
||||
]), async (ctx)=>{
|
||||
ctx.body = await MediaHelpers.uploadMedia(ctx).then((p)=>FileConverter.encode(p));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { MiscHelpers } from "../helpers/misc.js";
|
||||
import { argsToBools, limitToInt } from "./timeline.js";
|
||||
import { Announcements } from "../../../../models/index.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { MastoApiError } from "../middleware/catch-errors.js";
|
||||
import { filterContext } from "../middleware/filter-context.js";
|
||||
export function setupEndpointsMisc(router) {
|
||||
router.get("/v1/custom_emojis", async (ctx)=>{
|
||||
ctx.body = await MiscHelpers.getCustomEmoji();
|
||||
});
|
||||
router.get("/v1/instance", async (ctx)=>{
|
||||
ctx.body = await MiscHelpers.getInstance(ctx);
|
||||
});
|
||||
router.get("/v1/announcements", auth(true), async (ctx)=>{
|
||||
const args = argsToBools(ctx.query, [
|
||||
'with_dismissed'
|
||||
]);
|
||||
ctx.body = await MiscHelpers.getAnnouncements(args['with_dismissed'], ctx);
|
||||
});
|
||||
router.post("/v1/announcements/:id/dismiss", auth(true, [
|
||||
'write:accounts'
|
||||
]), async (ctx)=>{
|
||||
const announcement = await Announcements.findOneBy({
|
||||
id: ctx.params.id
|
||||
});
|
||||
if (!announcement) throw new MastoApiError(404);
|
||||
await MiscHelpers.dismissAnnouncement(announcement, ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
//FIXME: add link pagination to trends (ref: https://mastodon.social/api/v1/trends/tags?offset=10&limit=1)
|
||||
router.get([
|
||||
"/v1/trends/tags",
|
||||
"/v1/trends"
|
||||
], async (ctx)=>{
|
||||
const args = limitToInt(ctx.query);
|
||||
ctx.body = await MiscHelpers.getTrendingHashtags(args.limit, args.offset);
|
||||
//FIXME: convert ids
|
||||
});
|
||||
router.get("/v1/trends/statuses", filterContext('public'), async (ctx)=>{
|
||||
const args = limitToInt(ctx.query);
|
||||
ctx.body = await MiscHelpers.getTrendingStatuses(args.limit, args.offset, ctx);
|
||||
});
|
||||
router.get("/v1/trends/links", async (ctx)=>{
|
||||
ctx.body = [];
|
||||
});
|
||||
router.get("/v1/preferences", auth(true, [
|
||||
'read:accounts'
|
||||
]), async (ctx)=>{
|
||||
ctx.body = await MiscHelpers.getPreferences(ctx);
|
||||
});
|
||||
router.get("/v2/suggestions", auth(true, [
|
||||
'read:accounts'
|
||||
]), async (ctx)=>{
|
||||
const args = limitToInt(ctx.query);
|
||||
ctx.body = await MiscHelpers.getFollowSuggestions(args.limit, ctx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { NotificationHelpers } from "../helpers/notification.js";
|
||||
import { NotificationConverter } from "../converters/notification.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { filterContext } from "../middleware/filter-context.js";
|
||||
export function setupEndpointsNotifications(router) {
|
||||
router.get("/v1/notifications", auth(true, [
|
||||
'read:notifications'
|
||||
]), filterContext('notifications'), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query), [
|
||||
'types[]',
|
||||
'exclude_types[]'
|
||||
]);
|
||||
const res = await NotificationHelpers.getNotifications(args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id, ctx);
|
||||
ctx.body = await NotificationConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/notifications/:id", auth(true, [
|
||||
'read:notifications'
|
||||
]), filterContext('notifications'), async (ctx)=>{
|
||||
const notification = await NotificationHelpers.getNotificationOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NotificationConverter.encode(notification, ctx);
|
||||
});
|
||||
router.post("/v1/notifications/clear", auth(true, [
|
||||
'write:notifications'
|
||||
]), async (ctx)=>{
|
||||
await NotificationHelpers.clearAllNotifications(ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
router.post("/v1/notifications/:id/dismiss", auth(true, [
|
||||
'write:notifications'
|
||||
]), async (ctx)=>{
|
||||
const notification = await NotificationHelpers.getNotificationOr404(ctx.params.id, ctx);
|
||||
await NotificationHelpers.dismissNotification(notification.id, ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
router.post("/v1/conversations/:id/read", auth(true, [
|
||||
'write:conversations'
|
||||
]), async (ctx, reply)=>{
|
||||
await NotificationHelpers.markConversationAsRead(ctx.params.id, ctx);
|
||||
ctx.body = {};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { argsToBools, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { SearchHelpers } from "../helpers/search.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
export function setupEndpointsSearch(router) {
|
||||
router.get([
|
||||
"/v1/search",
|
||||
"/v2/search"
|
||||
], auth(true, [
|
||||
'read:search'
|
||||
]), async (ctx)=>{
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query), [
|
||||
'resolve',
|
||||
'following',
|
||||
'exclude_unreviewed'
|
||||
]));
|
||||
ctx.body = await SearchHelpers.search(args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, ctx);
|
||||
if (ctx.path === "/v1/search") {
|
||||
ctx.body = {
|
||||
...ctx.body,
|
||||
hashtags: ctx.body.hashtags.map((p)=>p.name)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { NoteConverter } from "../converters/note.js";
|
||||
import { NoteHelpers } from "../helpers/note.js";
|
||||
import { limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { UserConverter } from "../converters/user.js";
|
||||
import { PollHelpers } from "../helpers/poll.js";
|
||||
import { toArray } from "../../../../prelude/array.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { MastoApiError } from "../middleware/catch-errors.js";
|
||||
import { filterContext } from "../middleware/filter-context.js";
|
||||
export function setupEndpointsStatus(router) {
|
||||
router.post("/v1/statuses", auth(true, [
|
||||
'write:statuses'
|
||||
]), async (ctx)=>{
|
||||
const key = NoteHelpers.getIdempotencyKey(ctx);
|
||||
if (key !== null) {
|
||||
const result = await NoteHelpers.getFromIdempotencyCache(key);
|
||||
if (result) {
|
||||
ctx.body = result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let request = NoteHelpers.normalizeComposeOptions(ctx.request.body);
|
||||
ctx.body = await NoteHelpers.createNote(request, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
if (key !== null) NoteHelpers.postIdempotencyCache.set(key, {
|
||||
status: ctx.body
|
||||
});
|
||||
});
|
||||
router.put("/v1/statuses/:id", auth(true, [
|
||||
'write:statuses'
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
let request = NoteHelpers.normalizeEditOptions(ctx.request.body);
|
||||
ctx.body = await NoteHelpers.editNote(request, note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.get("/v1/statuses/:id", auth(false, [
|
||||
"read:statuses"
|
||||
]), filterContext('thread'), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteConverter.encode(note, ctx);
|
||||
});
|
||||
router.delete("/v1/statuses/:id", auth(true, [
|
||||
'write:statuses'
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.deleteNote(note, ctx);
|
||||
});
|
||||
router.get("/v1/statuses/:id/context", auth(false, [
|
||||
"read:statuses"
|
||||
]), filterContext('thread'), async (ctx)=>{
|
||||
//FIXME: determine final limits within helper functions instead of here
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
const ancestors = await NoteHelpers.getNoteAncestors(note, ctx.user ? 4096 : 60, ctx).then((n)=>NoteConverter.encodeMany(n, ctx));
|
||||
const descendants = await NoteHelpers.getNoteDescendants(note, ctx.user ? 4096 : 40, ctx.user ? 4096 : 20, ctx).then((n)=>NoteConverter.encodeMany(n, ctx));
|
||||
ctx.body = {
|
||||
ancestors,
|
||||
descendants
|
||||
};
|
||||
});
|
||||
router.get("/v1/statuses/:id/history", auth(false, [
|
||||
"read:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.getNoteEditHistory(note, ctx);
|
||||
});
|
||||
router.get("/v1/statuses/:id/source", auth(true, [
|
||||
"read:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = NoteHelpers.getNoteSource(note);
|
||||
});
|
||||
router.get("/v1/statuses/:id/reblogged_by", auth(false, [
|
||||
"read:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await NoteHelpers.getNoteRebloggedBy(note, args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/statuses/:id/favourited_by", auth(false, [
|
||||
"read:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await NoteHelpers.getNoteFavoritedBy(note, args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await UserConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.post("/v1/statuses/:id/favourite", auth(true, [
|
||||
"write:favourites"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
const reaction = await NoteHelpers.getDefaultReaction();
|
||||
ctx.body = await NoteHelpers.reactToNote(note, reaction, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/unfavourite", auth(true, [
|
||||
"write:favourites"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.removeReactFromNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/reblog", auth(true, [
|
||||
"write:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.reblogNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/unreblog", auth(true, [
|
||||
"write:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.unreblogNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/bookmark", auth(true, [
|
||||
"write:bookmarks"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.bookmarkNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/unbookmark", auth(true, [
|
||||
"write:bookmarks"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.unbookmarkNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/pin", auth(true, [
|
||||
"write:accounts"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.pinNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/unpin", auth(true, [
|
||||
"write:accounts"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.unpinNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/react/:name", auth(true, [
|
||||
"write:favourites"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.reactToNote(note, ctx.params.name, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.post("/v1/statuses/:id/unreact/:name", auth(true, [
|
||||
"write:favourites"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await NoteHelpers.removeReactFromNote(note, ctx).then((p)=>NoteConverter.encode(p, ctx));
|
||||
});
|
||||
router.get("/v1/polls/:id", auth(false, [
|
||||
"read:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
ctx.body = await PollHelpers.getPoll(note, ctx);
|
||||
});
|
||||
router.post("/v1/polls/:id/votes", auth(true, [
|
||||
"write:statuses"
|
||||
]), async (ctx)=>{
|
||||
const note = await NoteHelpers.getNoteOr404(ctx.params.id, ctx);
|
||||
const body = ctx.request.body;
|
||||
const choices = toArray(body.choices ?? []).map((p)=>parseInt(p));
|
||||
if (choices.length < 1) throw new MastoApiError(400, "Must vote for at least one option");
|
||||
ctx.body = await PollHelpers.voteInPoll(choices, note, ctx);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function setupEndpointsStreaming(router) {
|
||||
router.get("/v1/streaming/health", async (ctx)=>{
|
||||
ctx.body = "OK";
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { TimelineHelpers } from "../helpers/timeline.js";
|
||||
import { NoteConverter } from "../converters/note.js";
|
||||
import { UserLists } from "../../../../models/index.js";
|
||||
import { auth } from "../middleware/auth.js";
|
||||
import { MastoApiError } from "../middleware/catch-errors.js";
|
||||
import { filterContext } from "../middleware/filter-context.js";
|
||||
//TODO: Move helper functions to a helper class
|
||||
export function limitToInt(q, additional = []) {
|
||||
let object = q;
|
||||
if (q.limit) {
|
||||
if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
|
||||
}
|
||||
if (q.offset) {
|
||||
if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10);
|
||||
}
|
||||
for (const key of additional)if (typeof q[key] === "string") object[key] = parseInt(q[key], 10);
|
||||
return object;
|
||||
}
|
||||
export function argsToBools(q, additional = []) {
|
||||
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||
const toBoolean = (value)=>![
|
||||
"0",
|
||||
"f",
|
||||
"F",
|
||||
"false",
|
||||
"FALSE",
|
||||
"off",
|
||||
"OFF"
|
||||
].includes(value);
|
||||
// Keys taken from:
|
||||
// - https://docs.joinmastodon.org/methods/accounts/#statuses
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#public
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#tag
|
||||
let keys = [
|
||||
'only_media',
|
||||
'exclude_replies',
|
||||
'exclude_reblogs',
|
||||
'pinned',
|
||||
'local',
|
||||
'remote'
|
||||
].concat(additional);
|
||||
let object = q;
|
||||
for (const key of keys)if (q[key] && typeof q[key] === "string") object[key] = toBoolean(q[key]);
|
||||
return object;
|
||||
}
|
||||
export function normalizeUrlQuery(q, arrayKeys = []) {
|
||||
const dict = {};
|
||||
for(const k in q){
|
||||
if (arrayKeys.includes(k)) dict[k] = Array.isArray(q[k]) ? q[k] : [
|
||||
q[k]
|
||||
];
|
||||
else dict[k] = Array.isArray(q[k]) ? q[k]?.at(-1) : q[k];
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
export function setupEndpointsTimeline(router) {
|
||||
router.get("/v1/timelines/public", auth(true, [
|
||||
'read:statuses'
|
||||
]), filterContext('public'), async (ctx, reply)=>{
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)));
|
||||
const res = await TimelineHelpers.getPublicTimeline(args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/timelines/tag/:hashtag", auth(false, [
|
||||
'read:statuses'
|
||||
]), filterContext('public'), async (ctx, reply)=>{
|
||||
const tag = (ctx.params.hashtag ?? '').trim().toLowerCase();
|
||||
const args = normalizeUrlQuery(argsToBools(limitToInt(ctx.query)), [
|
||||
'any[]',
|
||||
'all[]',
|
||||
'none[]'
|
||||
]);
|
||||
const res = await TimelineHelpers.getTagTimeline(tag, args.max_id, args.since_id, args.min_id, args.limit, args['any[]'] ?? [], args['all[]'] ?? [], args['none[]'] ?? [], args.only_media, args.local, args.remote, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/timelines/home", auth(true, [
|
||||
'read:statuses'
|
||||
]), filterContext('home'), async (ctx, reply)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await TimelineHelpers.getHomeTimeline(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/timelines/list/:listId", auth(true, [
|
||||
'read:lists'
|
||||
]), filterContext('home'), async (ctx, reply)=>{
|
||||
const list = await UserLists.findOneBy({
|
||||
userId: ctx.user.id,
|
||||
id: ctx.params.listId
|
||||
});
|
||||
if (!list) throw new MastoApiError(404);
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
const res = await TimelineHelpers.getListTimeline(list, args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
ctx.body = await NoteConverter.encodeMany(res, ctx);
|
||||
});
|
||||
router.get("/v1/conversations", auth(true, [
|
||||
'read:statuses'
|
||||
]), async (ctx, reply)=>{
|
||||
const args = normalizeUrlQuery(limitToInt(ctx.query));
|
||||
ctx.body = await TimelineHelpers.getConversations(args.max_id, args.since_id, args.min_id, args.limit, ctx);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user