import { AVATAR_SVG, HEADER_SVG, activityObject, actor, followersCollection, followingCollection, hostMeta, inboxHandler, nodeInfo, nodeInfoLinks, outbox, webFinger } from "./activitypub"; import { ensureAdminUser } from "./db"; import { HttpError, cors, json, svgResponse } from "./http"; import { accountStatuses, accountFollowers, accountFollowing, addListAccounts, authorize, authorizeFollowRequest, authorizePage, bookmarkStatus, bookmarksList, createApp, createList, createPushSubscription, createStatus, customEmojis, deleteList, deletePushSubscription, deleteScheduledStatus, deleteStatusEndpoint, favouriteStatus, favouritesList, filtersV1, followAccount, followRequestsList, getAccount, getList, getPoll, getPushSubscription, getRelationships, getScheduledStatus, getStatusEndpoint, hashtagInfo, hashtagTimeline, homeTimeline, instance, instanceV2, listAccounts, listScheduledStatuses, listTimeline, listsList, lookupAccount, markersList, notificationClear, notificationDismiss, notificationsList, pinStatus, publishDueScheduledStatuses, publicTimeline, reblogStatus, rejectFollowRequest, removeListAccounts, revoke, search, serveMedia, statusContext, token, trendsTags, unbookmarkStatus, unfavouriteStatus, unfollowAccount, unpinStatus, unreblogStatus, updateList, updatePushSubscription, updateScheduledStatus, updateCredentials, updateMedia, uploadMedia, votePoll, verifyAppCredentials, verifyCredentials } from "./mastodon"; import { processOutgoingDeliveries } from "./federation"; export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { try { await ensureAdminUser(env); const response = await route(request, env); ctx.waitUntil(processOutgoingDeliveries(env)); return response; } catch (error) { if (error instanceof HttpError) return json({ error: error.message }, error.status); console.error("unhandled", error); return json({ error: "internal_server_error" }, 500); } }, async scheduled(_event: ScheduledEvent, env: Env): Promise { await ensureAdminUser(env); await publishDueScheduledStatuses(env); await processOutgoingDeliveries(env); } }; async function route(request: Request, env: Env): Promise { if (request.method === "OPTIONS") return cors(new Response(null, { status: 204 })); const url = new URL(request.url); const path = url.pathname.replace(/\/+$/, "") || "/"; const method = request.method.toUpperCase(); if (method === "GET" && path === "/") return nodeInfo(env); if (method === "GET" && path === "/avatar.png") return svgResponse(AVATAR_SVG); if (method === "GET" && path === "/header.png") return svgResponse(HEADER_SVG); if (method === "GET" && path === "/.well-known/webfinger") return webFinger(request, env); if (method === "GET" && path === "/.well-known/nodeinfo") return nodeInfoLinks(env); if (method === "GET" && path === "/.well-known/host-meta") return hostMeta(env); if (method === "GET" && path === "/nodeinfo/2.0") return nodeInfo(env); if (method === "GET" && path === "/api/v1/instance") return instance(env); if (method === "GET" && path === "/api/v2/instance") return instanceV2(env); if (method === "POST" && path === "/api/v1/apps") return createApp(request, env); if (method === "GET" && path === "/api/v1/apps/verify_credentials") return verifyAppCredentials(request, env); if (method === "GET" && path === "/oauth/authorize") return authorizePage(request, env); if (method === "POST" && path === "/oauth/authorize") return authorize(request, env); if (method === "POST" && path === "/oauth/token") return token(request, env); if (method === "POST" && path === "/oauth/revoke") return revoke(request, env); if (method === "GET" && path === "/api/v1/accounts/verify_credentials") return verifyCredentials(request, env); if ((method === "PATCH" || method === "POST") && path === "/api/v1/accounts/update_credentials") return updateCredentials(request, env); if (method === "GET" && path === "/api/v1/accounts/relationships") return getRelationships(request, env); if (method === "GET" && path === "/api/v1/accounts/search") return search(request, env); if (method === "GET" && path === "/api/v1/accounts/lookup") return lookupAccount(request, env); let m: RegExpMatchArray | null; if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)$/))) return getAccount(env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/statuses$/))) return accountStatuses(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/followers$/))) return accountFollowers(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/following$/))) return accountFollowing(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/follow$/))) return followAccount(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/accounts\/([^/]+)\/unfollow$/))) return unfollowAccount(request, env, decodeURIComponent(m[1])); if (method === "GET" && path === "/api/v1/follow_requests") return followRequestsList(request, env); if (method === "POST" && (m = path.match(/^\/api\/v1\/follow_requests\/([^/]+)\/authorize$/))) return authorizeFollowRequest(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/follow_requests\/([^/]+)\/reject$/))) return rejectFollowRequest(request, env, decodeURIComponent(m[1])); if (method === "POST" && path === "/api/v1/statuses") return createStatus(request, env); if (method === "GET" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)$/))) return getStatusEndpoint(request, env, decodeURIComponent(m[1])); if (method === "DELETE" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)$/))) return deleteStatusEndpoint(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/context$/))) return statusContext(env, decodeURIComponent(m[1]), request); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/favourite$/))) return favouriteStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/unfavourite$/))) return unfavouriteStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/reblog$/))) return reblogStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/unreblog$/))) return unreblogStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/bookmark$/))) return bookmarkStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/unbookmark$/))) return unbookmarkStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/pin$/))) return pinStatus(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/statuses\/([^/]+)\/unpin$/))) return unpinStatus(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/polls\/([^/]+)$/))) return getPoll(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/polls\/([^/]+)\/votes$/))) return votePoll(request, env, decodeURIComponent(m[1])); if (method === "GET" && path === "/api/v1/scheduled_statuses") return listScheduledStatuses(request, env); if (method === "GET" && (m = path.match(/^\/api\/v1\/scheduled_statuses\/([^/]+)$/))) return getScheduledStatus(request, env, decodeURIComponent(m[1])); if ((method === "PUT" || method === "PATCH") && (m = path.match(/^\/api\/v1\/scheduled_statuses\/([^/]+)$/))) return updateScheduledStatus(request, env, decodeURIComponent(m[1])); if (method === "DELETE" && (m = path.match(/^\/api\/v1\/scheduled_statuses\/([^/]+)$/))) return deleteScheduledStatus(request, env, decodeURIComponent(m[1])); if (method === "GET" && path === "/api/v1/timelines/public") return publicTimeline(request, env); if (method === "GET" && path === "/api/v1/timelines/home") return homeTimeline(request, env); if (method === "GET" && (m = path.match(/^\/api\/v1\/timelines\/list\/([^/]+)$/))) return listTimeline(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/timelines\/tag\/([^/]+)$/))) return hashtagTimeline(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/tags\/([^/]+)$/))) return hashtagInfo(env, decodeURIComponent(m[1])); if (method === "GET" && path === "/api/v1/lists") return listsList(request, env); if (method === "POST" && path === "/api/v1/lists") return createList(request, env); if (method === "GET" && (m = path.match(/^\/api\/v1\/lists\/([^/]+)$/))) return getList(request, env, decodeURIComponent(m[1])); if ((method === "PUT" || method === "PATCH") && (m = path.match(/^\/api\/v1\/lists\/([^/]+)$/))) return updateList(request, env, decodeURIComponent(m[1])); if (method === "DELETE" && (m = path.match(/^\/api\/v1\/lists\/([^/]+)$/))) return deleteList(request, env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/api\/v1\/lists\/([^/]+)\/accounts$/))) return listAccounts(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/api\/v1\/lists\/([^/]+)\/accounts$/))) return addListAccounts(request, env, decodeURIComponent(m[1])); if (method === "DELETE" && (m = path.match(/^\/api\/v1\/lists\/([^/]+)\/accounts$/))) return removeListAccounts(request, env, decodeURIComponent(m[1])); if (method === "GET" && path === "/api/v1/bookmarks") return bookmarksList(request, env); if (method === "GET" && path === "/api/v1/favourites") return favouritesList(request, env); if (method === "POST" && (path === "/api/v1/media" || path === "/api/v2/media")) return uploadMedia(request, env); if (method === "PUT" && (m = path.match(/^\/api\/v1\/media\/([^/]+)$/))) return updateMedia(request, env, decodeURIComponent(m[1])); if (method === "GET" && path === "/api/v1/notifications") return notificationsList(request, env); if (method === "POST" && path === "/api/v1/notifications/clear") return notificationClear(request, env); if (method === "POST" && (m = path.match(/^\/api\/v1\/notifications\/([^/]+)\/dismiss$/))) return notificationDismiss(request, env, decodeURIComponent(m[1])); if (method === "GET" && (path === "/api/v2/search" || path === "/api/v1/search")) return search(request, env); if (method === "GET" && path === "/api/v1/custom_emojis") return customEmojis(env); if (method === "GET" && path === "/api/v1/filters") return filtersV1(request, env); if (method === "GET" && path === "/api/v1/trends/tags") return trendsTags(env); if (method === "GET" && path === "/api/v1/markers") return markersList(request, env); if (method === "GET" && path === "/api/v1/push/subscription") return getPushSubscription(request, env); if (method === "POST" && path === "/api/v1/push/subscription") return createPushSubscription(request, env); if (method === "PUT" && path === "/api/v1/push/subscription") return updatePushSubscription(request, env); if (method === "DELETE" && path === "/api/v1/push/subscription") return deletePushSubscription(request, env); if (method === "GET" && (m = path.match(/^\/media\/(.+)$/))) return serveMedia(env, m[1]); if (method === "GET" && (m = path.match(/^\/users\/([^/]+)$/))) return actor(env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/users\/([^/]+)\/outbox$/))) return outbox(request, env, decodeURIComponent(m[1])); if (method === "POST" && (m = path.match(/^\/users\/([^/]+)\/inbox$/))) return inboxHandler(request, env, decodeURIComponent(m[1])); if (method === "POST" && path === "/inbox") return inboxHandler(request, env, null); if (method === "GET" && (m = path.match(/^\/users\/([^/]+)\/followers$/))) return followersCollection(env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/users\/([^/]+)\/following$/))) return followingCollection(env, decodeURIComponent(m[1])); if (method === "GET" && (m = path.match(/^\/objects\/([^/]+)$/))) return activityObject(env, decodeURIComponent(m[1])); return json({ error: "not_found" }, 404); }