forked from FoundKeyGang/FoundKey
Johann150
808ad2a505
The default of the library used is to require either of the "date" or "x-date" headers. It does not seem sensible to pass on this default. It may be to fix federation with another software but that software should be considered broken, and it does not seem reasonable to potentially get an outdated signature just to fix federation with some buggy software.
267 lines
6.5 KiB
TypeScript
267 lines
6.5 KiB
TypeScript
import json from 'koa-json-body';
|
|
import Router from '@koa/router';
|
|
import { In, IsNull, Not } from 'typeorm';
|
|
import httpSignature from '@peertube/http-signature';
|
|
|
|
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
|
import renderNote from '@/remote/activitypub/renderer/note.js';
|
|
import renderKey from '@/remote/activitypub/renderer/key.js';
|
|
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
|
|
import renderEmoji from '@/remote/activitypub/renderer/emoji.js';
|
|
import { inbox as processInbox } from '@/queue/index.js';
|
|
import { isSelfHost } from '@/misc/convert-host.js';
|
|
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
|
|
import { ILocalUser, User } from '@/models/entities/user.js';
|
|
import { renderLike } from '@/remote/activitypub/renderer/like.js';
|
|
import { getUserKeypair } from '@/misc/keypair-store.js';
|
|
import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
|
import { renderNoteOrRenoteActivity } from '@/remote/activitypub/renderer/note-or-renote.js';
|
|
import Outbox from './activitypub/outbox.js';
|
|
import Followers from './activitypub/followers.js';
|
|
import Following from './activitypub/following.js';
|
|
import Featured from './activitypub/featured.js';
|
|
|
|
// Init router
|
|
const router = new Router();
|
|
|
|
function inbox(ctx: Router.RouterContext): void {
|
|
let signature;
|
|
|
|
try {
|
|
signature = httpSignature.parseRequest(ctx.req);
|
|
} catch (e) {
|
|
ctx.status = 401;
|
|
return;
|
|
}
|
|
|
|
processInbox(ctx.request.body, signature);
|
|
|
|
ctx.status = 202;
|
|
}
|
|
|
|
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
|
|
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
|
|
|
|
function isActivityPubReq(ctx: Router.RouterContext): boolean {
|
|
ctx.response.vary('Accept');
|
|
// if no accept header is supplied, koa returns the 1st, so html is used as a dummy
|
|
// i.e. activitypub requests must be explicit
|
|
const accepted = ctx.accepts('html', ACTIVITY_JSON, LD_JSON);
|
|
return typeof accepted === 'string' && !accepted.match(/html/);
|
|
}
|
|
|
|
export function setResponseType(ctx: Router.RouterContext): void {
|
|
const accept = ctx.accepts(ACTIVITY_JSON, LD_JSON);
|
|
if (accept === LD_JSON) {
|
|
ctx.response.type = LD_JSON;
|
|
} else {
|
|
ctx.response.type = ACTIVITY_JSON;
|
|
}
|
|
}
|
|
|
|
// inbox
|
|
router.post('/inbox', json(), inbox);
|
|
router.post('/users/:user/inbox', json(), inbox);
|
|
|
|
// note
|
|
router.get('/notes/:note', async (ctx, next) => {
|
|
if (!isActivityPubReq(ctx)) return await next();
|
|
|
|
const note = await Notes.findOneBy({
|
|
id: ctx.params.note,
|
|
visibility: In(['public' as const, 'home' as const]),
|
|
localOnly: false,
|
|
});
|
|
|
|
if (note == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
// redirect if remote
|
|
if (note.userHost != null) {
|
|
if (note.uri == null || isSelfHost(note.userHost)) {
|
|
ctx.status = 500;
|
|
return;
|
|
}
|
|
ctx.redirect(note.uri);
|
|
return;
|
|
}
|
|
|
|
ctx.body = renderActivity(await renderNote(note, false));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
});
|
|
|
|
// note activity
|
|
router.get('/notes/:note/activity', async ctx => {
|
|
if (!isActivityPubReq(ctx)) {
|
|
/*
|
|
Redirect to the human readable page. in this case using next is not possible,
|
|
since there is no human readable page explicitly for the activity.
|
|
*/
|
|
ctx.redirect(`/notes/${ctx.params.note}`);
|
|
return;
|
|
}
|
|
|
|
const note = await Notes.findOneBy({
|
|
id: ctx.params.note,
|
|
userHost: IsNull(),
|
|
visibility: In(['public' as const, 'home' as const]),
|
|
localOnly: false,
|
|
});
|
|
|
|
if (note == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
ctx.body = renderActivity(await renderNoteOrRenoteActivity(note));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
});
|
|
|
|
// outbox
|
|
router.get('/users/:user/outbox', Outbox);
|
|
|
|
// followers
|
|
router.get('/users/:user/followers', Followers);
|
|
|
|
// following
|
|
router.get('/users/:user/following', Following);
|
|
|
|
// featured
|
|
router.get('/users/:user/collections/featured', Featured);
|
|
|
|
// publickey
|
|
router.get('/users/:user/publickey', async ctx => {
|
|
const userId = ctx.params.user;
|
|
|
|
const user = await Users.findOneBy({
|
|
id: userId,
|
|
host: IsNull(),
|
|
});
|
|
|
|
if (user == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
const keypair = await getUserKeypair(user.id);
|
|
|
|
if (Users.isLocalUser(user)) {
|
|
ctx.body = renderActivity(renderKey(user, keypair));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
} else {
|
|
ctx.status = 400;
|
|
}
|
|
});
|
|
|
|
// user
|
|
async function userInfo(ctx: Router.RouterContext, user: User | null): Promise<void> {
|
|
if (user == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
}
|
|
|
|
router.get('/users/:user', async (ctx, next) => {
|
|
if (!isActivityPubReq(ctx)) return await next();
|
|
|
|
const userId = ctx.params.user;
|
|
|
|
const user = await Users.findOneBy({
|
|
id: userId,
|
|
host: IsNull(),
|
|
isSuspended: false,
|
|
});
|
|
|
|
await userInfo(ctx, user);
|
|
});
|
|
|
|
router.get('/@:user', async (ctx, next) => {
|
|
if (!isActivityPubReq(ctx)) return await next();
|
|
|
|
const user = await Users.findOneBy({
|
|
usernameLower: ctx.params.user.toLowerCase(),
|
|
host: IsNull(),
|
|
isSuspended: false,
|
|
});
|
|
|
|
await userInfo(ctx, user);
|
|
});
|
|
|
|
// emoji
|
|
router.get('/emojis/:emoji', async ctx => {
|
|
const emoji = await Emojis.findOneBy({
|
|
host: IsNull(),
|
|
name: ctx.params.emoji,
|
|
});
|
|
|
|
if (emoji == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
ctx.body = renderActivity(await renderEmoji(emoji));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
});
|
|
|
|
// like
|
|
router.get('/likes/:like', async ctx => {
|
|
const reaction = await NoteReactions.findOneBy({ id: ctx.params.like });
|
|
|
|
if (reaction == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
const note = await Notes.findOneBy({
|
|
id: reaction.noteId,
|
|
visibility: In(['public' as const, 'home' as const]),
|
|
});
|
|
|
|
if (note == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
ctx.body = renderActivity(await renderLike(reaction, note));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
});
|
|
|
|
// follow
|
|
router.get('/follows/:follower/:followee', async ctx => {
|
|
// This may be used before the follow is completed, so we do not
|
|
// check if the following exists.
|
|
|
|
const [follower, followee] = await Promise.all([
|
|
Users.findOneBy({
|
|
id: ctx.params.follower,
|
|
host: IsNull(),
|
|
}),
|
|
Users.findOneBy({
|
|
id: ctx.params.followee,
|
|
host: Not(IsNull()),
|
|
}),
|
|
]);
|
|
|
|
if (follower == null || followee == null) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
|
|
ctx.body = renderActivity(renderFollow(follower, followee));
|
|
ctx.set('Cache-Control', 'public, max-age=180');
|
|
setResponseType(ctx);
|
|
});
|
|
|
|
export default router;
|