diff --git a/.well-known/webfinger b/.well-known/webfinger deleted file mode 100644 index fa58dd8..0000000 --- a/.well-known/webfinger +++ /dev/null @@ -1,37 +0,0 @@ -{ - "aliases": [ - "https://coderrrrr.site/" - ], - "links": [ - { - "href": "https://coderrrrr.site/", - "rel": "http://webfinger.net/rel/profile-page", - "type": "text/html" - }, - { - "href": "https://web.brid.gy/coderrrrr.site", - "rel": "self", - "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - }, - { - "href": "https://web.brid.gy/coderrrrr.site", - "rel": "self", - "type": "application/activity+json" - }, - { - "href": "https://web.brid.gy/coderrrrr.site/inbox", - "rel": "inbox", - "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - }, - { - "href": "https://web.brid.gy/ap/sharedInbox", - "rel": "sharedInbox", - "type": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" - }, - { - "rel": "http://ostatus.org/schema/1.0/subscribe", - "template": "https://fed.brid.gy/web/coderrrrr.site?url={uri}" - } - ], - "subject": "acct:coderrrrr.site@coderrrrr.site" -} diff --git a/@coder.json b/@coder.json deleted file mode 100644 index fdacee8..0000000 --- a/@coder.json +++ /dev/null @@ -1 +0,0 @@ -{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","toot":"http://joinmastodon.org/ns#","featured":{"@id":"toot:featured","@type":"@id"},"featuredTags":{"@id":"toot:featuredTags","@type":"@id"},"alsoKnownAs":{"@id":"as:alsoKnownAs","@type":"@id"},"movedTo":{"@id":"as:movedTo","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","discoverable":"toot:discoverable","suspended":"toot:suspended","memorial":"toot:memorial","indexable":"toot:indexable","attributionDomains":{"@id":"toot:attributionDomains","@type":"@id"},"focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://usr.cloud/users/coder","type":"Person","following":"https://usr.cloud/users/coder/following","followers":"https://usr.cloud/users/coder/followers","inbox":"https://usr.cloud/users/coder/inbox","outbox":"https://usr.cloud/users/coder/outbox","featured":"https://usr.cloud/users/coder/collections/featured","featuredTags":"https://usr.cloud/users/coder/collections/tags","preferredUsername":"coder","name":"Deva Midhun ☁️","summary":"\u003cp\u003eHI!!! I am Deva!!\u003c/p\u003e","url":"https://usr.cloud/@coder","manuallyApprovesFollowers":false,"discoverable":false,"indexable":false,"published":"2025-01-13T00:00:00Z","memorial":false,"attributionDomains":["coderrrrr.site","blog.coderrrrr.site"],"publicKey":{"id":"https://usr.cloud/users/coder#main-key","owner":"https://usr.cloud/users/coder","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nvxsmoBuFlxmNxi7u/1\nhdsy6cnjmoeVH4gQYOVs13mUcttVV02Q57wocqa8wdArYzDJ7ld5gI1ySwdwpj+H\nl879lq1DZOjNCTjLDr8MLQ3cy3dm2hn6s7jA8hIw/CbED3YEGB/G8y776gXTFEhg\nu+6+Pckx0QKzrzwIJPWbVRN4eprciM4GVGPwkAMPal3EakxRmHbtvnCwF62yAZhW\noudNi41G2KOS+zwHnhxP3AunsrFtEf3Djr3S0+3N1sEOtEjSJ/ck42cvhvMmlact\n+HpBlfZWyuJ4vlhsvcwWqSQlzJ2JC5CGr4ugLqAM6E8DaNJAtdv+Nyw1srjePoWs\n3wIDAQAB\n-----END PUBLIC KEY-----\n"},"tag":[],"attachment":[{"type":"PropertyValue","name":"OpenPGP","value":"openpgp4fpr:6389542B98CB868DAC73A373ED1190B780583CF6"},{"type":"PropertyValue","name":"Website","value":"\u003ca href=\"https://coderrrrr.site\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003ecoderrrrr.site\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"}],"endpoints":{"sharedInbox":"https://usr.cloud/inbox"},"icon":{"type":"Image","mediaType":"image/png","url":"https://files.usr.cloud/v1/AUTH_f22cbcf5b3904990be9696691ff73fc6/files.usr.cloud/accounts/avatars/113/822/701/816/479/941/original/7d7f6088ba1ddd57.png"},"image":{"type":"Image","mediaType":"image/png","url":"https://files.usr.cloud/v1/AUTH_f22cbcf5b3904990be9696691ff73fc6/files.usr.cloud/accounts/headers/113/822/701/816/479/941/original/e0ebedf722cf0a39.png"}} diff --git a/api/activitypub/actor.ts b/api/activitypub/actor.ts new file mode 100644 index 0000000..27b82cb --- /dev/null +++ b/api/activitypub/actor.ts @@ -0,0 +1,39 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +export default function (req: VercelRequest, res: VercelResponse) { + const { headers } = req; + + if ("accept" in headers) { + const accept = headers["accept"]; + if (accept != null && accept.split(",").indexOf("text/html") > -1) { + return res.redirect(302, "https://coderrrrr.site/").end(); + } + } + + res.statusCode = 200; + res.setHeader("Content-Type", `application/activity+json`); + res.json({ + "@context": ["https://www.w3.org/ns/activitystreams", { "@language": "en- US" }], + "type": "Person", + "id": "https://coderrrrr.site/coder", + "outbox": "https://coderrrrr.site/outbox", + "following": "https://coderrrrr.site/following", + "followers": "https://coderrrrr.site/followers", + "inbox": "https://coderrrrr.site/inbox", + "preferredUsername": "coder", + "name": "Deva Midhun's blog", + "summary": "", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://i.ibb.co/N6J5b8WS/download20250102015611.png" + }, + "publicKey": { + "@context": "https://w3id.org/security/v1", + "@type": "Key", + "id": "https://coderrrrr.site/coder#main-key", + "owner": "https://coderrrrr.site/coder", + "publicKeyPem": process.env.ACTIVITYPUB_PUBLIC_KEY + } + }); +} diff --git a/api/activitypub/authorize_interaction.ts b/api/activitypub/authorize_interaction.ts new file mode 100644 index 0000000..0ca4e5d --- /dev/null +++ b/api/activitypub/authorize_interaction.ts @@ -0,0 +1,7 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +export default function (req: VercelRequest, res: VercelResponse) { + res.statusCode = 200; + res.setHeader("Content-Type", `application/jrd+json`); + res.end('ok'); +}; diff --git a/api/activitypub/followers.ts b/api/activitypub/followers.ts new file mode 100644 index 0000000..c9294f1 --- /dev/null +++ b/api/activitypub/followers.ts @@ -0,0 +1,31 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import admin from 'firebase-admin'; + +if (!admin.apps.length) { + admin.initializeApp({ + credential: admin.credential.cert({ + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n') + }) + }); +} + +const db = admin.firestore(); + +export default async function (req: VercelRequest, res: VercelResponse) { + const collection = db.collection('followers'); + const actors = await collection.select("actor").get(); + + const output = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://status.kinlan.me/users/coder/following?page=1", + "type": "OrderedCollectionPage", + "totalItems": actors.docs.length, + "orderedItems": actors.docs.map(item=>item.get("actor")) + } + + res.statusCode = 200; + res.setHeader("Content-Type", `application/activity+json`); + res.json(output); +}; diff --git a/api/activitypub/following.ts b/api/activitypub/following.ts new file mode 100644 index 0000000..3a69ebd --- /dev/null +++ b/api/activitypub/following.ts @@ -0,0 +1,7 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +export default function (req: VercelRequest, res: VercelResponse) { + res.statusCode = 200; + res.setHeader("Content-Type", `application/activity+json`); + res.end('ok'); +}; diff --git a/api/activitypub/inbox.ts b/api/activitypub/inbox.ts new file mode 100644 index 0000000..a702ace --- /dev/null +++ b/api/activitypub/inbox.ts @@ -0,0 +1,347 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import { AP } from 'activitypub-core-types'; +import { CoreObject, EntityReference } from 'activitypub-core-types/lib/activitypub/index.js'; + +import admin from 'firebase-admin'; + +import type { Readable } from 'node:stream'; +import { v4 as uuid } from 'uuid'; +import { fetchActorInformation } from '../../lib/activitypub/utils/fetchActorInformation.js'; +import { parseSignature } from '../../lib/activitypub/utils/parseSignature.js'; +import { sendSignedRequest } from '../../lib/activitypub/utils/sendSignedRequest.js'; +import { verifySignature } from '../../lib/activitypub/utils/verifySignature.js'; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +if (!admin.apps.length) { + admin.initializeApp({ + credential: admin.credential.cert({ + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n') + }) + }); +} + +const db = admin.firestore(); + +async function buffer(readable: Readable) { + const chunks = []; + for await (const chunk of readable) { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + } + return Buffer.concat(chunks); +} + +export default async function (req: VercelRequest, res: VercelResponse) { + const { body, query, method, url, headers } = req; + + res.statusCode = 200; + res.setHeader("Content-Type", `application/activity+json`); + + // Verify the message some how. + const buf = await buffer(req); + const rawBody = buf.toString('utf8'); + + const message = JSON.parse(rawBody); + + if (message.type == "Delete") { + // Ignore deletes for now. + return res.end("delete") + } + + console.log(message); + + const signature = parseSignature(req); + const actorInformation = await fetchActorInformation(signature.keyId); + + if(actorInformation == undefined || actorInformation == null) { + console.log("Actor information is null"); + res.end('actor information is null'); + return; + } + + const signatureValid = verifySignature(signature, actorInformation.publicKey); + + if (signatureValid == null || signatureValid == false) { + console.log("invalid signature"); + res.end('invalid signature'); + return; + } + + if (actorInformation != null) { + await saveActor(actorInformation); + // Add the actor information to the message so that it's saved directly. + message.actor = actorInformation; + } + + console.log(message.type); + + // We should check the digest. + if (message.type == "Follow" && actorInformation != null) { + // We are following. + await saveFollow(message, actorInformation); + } + + if (message.type == "Like") { + await saveLike(message); + } + + if (message.type == "Announce") { + await saveAnnounce(message); + } + + if (message.type == "Create") { + console.log("Message type Create") + // Someone is sending us a message. + const createMessage = message; + + if (createMessage == null || createMessage.id == null) return; + if (createMessage.object == null) return; + // We only interested in Replies - that is a "note" with a "replyTo" + const createObject = createMessage.object + if (createObject.type == "Note" && createObject.inReplyTo != undefined) { + await saveReply(createMessage); + } + } + + if (message.type == "Undo") { + // Undo a follow. + const undoObject: AP.Undo = message; + if (undoObject == null || undoObject.id == null) return; + if (undoObject.object == null) return; + if ("actor" in undoObject.object == false) return; + + if ((undoObject.object).type == "Follow") { + await removeFollow(undoObject); + } + + if ((undoObject.object).type == "Like") { + await removeLike(undoObject); + } + + if ((undoObject.object).type == "Announce") { + await removeAnnounce(undoObject); + } + } + + if (message.type == "Update") { + // TODO: We need to update the messages + console.log("Update message", message); + } + + res.end("ok"); +}; + +const getActorId = (actor: AP.EntityReference): string => { + if (typeof actor == "string") { + return actor; + } + else if (actor instanceof URL) { + return actor.toString() + } + else { + return (actor.id || "").toString(); + } +} + +async function removeFollow(message: AP.Follow) { + // If from Mastodon - someone unfollowed me, we need to delete it from the store. + const docId = getActorId(message.actor).replace(/\//g, "_"); + + console.log("DocId to delete", docId); + + const res = await db.collection('followers').doc(docId).delete(); + + console.log("Deleted", res); +} + +async function removeLike(message: AP.Like) { + // If from Mastodon - someone un-liked the post. We need to delete it from the store. + /* + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://coderrrrr.site/coder#likes/854/undo', + type: 'Undo', + actor: 'https://coderrrrr.site/coder', + object: { + id: 'https://coderrrrr.site/coder#likes/854', + type: 'Like', + actor: 'https://coderrrrr.site/coder', + object: 'https://coderrrrr.site/thoughts-on-web-follow/' + } + } + */ + const doc = message.object?.object.toString().replace(/\//g, "_"); + const actorId = message.object?.id.toString().replace(/\//g, "_"); + + console.log(`Attempting to delete Like ${actorId} on ${doc}`); + + const res = await db.collection('likes').doc(doc).collection('messages').doc(actorId).delete(); + + console.log(`Deleted Like ${actorId} on ${doc}`, res); +} + +async function removeAnnounce(message: AP.Announce) { + // If from Mastodon - someone un-liked the post. We need to delete it from the store. + const object = message.object as EntityReference; + const doc = object?.object.toString().replace(/\//g, "_"); + const actorId = object?.id.toString().replace(/\//g, "_"); + + console.log(`Attempting to delete Announce ${actorId} on ${doc}`); + + const res = await db.collection('announces').doc(doc).collection('messages').doc(actorId).delete(); + + console.log(`Deleted Announce ${actorId} on ${doc}`, res); +} + +// Save the Actor objects so that we have a cache of them. +// Note: We will be embeddeding the actor information in the messages for saving directly so we can do less reads. +async function saveActor(message: AP.Actor) { + if (message.id == null) return; + const collection = db.collection('actors'); + const actorId = message.id.toString().replace(/\//g, "_"); + + const actorDocRef = collection.doc(actorId); + + // Create the follow; + await actorDocRef.set(message); // Always update the actor. +} + +async function saveFollow(message: AP.Follow, actorInformation: AP.Actor) { + if (message.id == null) return; + + const collection = db.collection('followers'); + + const actorID = getActorId(message.actor).toString(); + const followDocRef = collection.doc(actorID.replace(/\//g, "_")); + const followDoc = await followDocRef.get(); + + if (followDoc.exists) { + console.log("Already Following"); + return; + } + + // Create the follow; + await followDocRef.set(message); + + const guid = uuid(); + const domain = 'coderrrrr.site'; + + const acceptRequest: AP.Accept = { + "@context": "https://www.w3.org/ns/activitystreams", + 'id': `https://${domain}/${guid}`, + 'type': 'Accept', + 'actor': "https://coderrrrr.site/coder", + 'object': (message.actor as CoreObject).id + }; + + const actorInbox = new URL(actorInformation.inbox); + + const response = await sendSignedRequest(actorInbox, acceptRequest); + + console.log("Following result", response.status, response.statusText, await response.text()); +} + +async function saveLike(message: AP.Like) { + // If from Mastodon - someone liked the post. + const collection = db.collection('likes'); + + // We should do some checks + // 1. TODO: in reply to is against a post that I made. + + console.log("Save Like", message); + + /* + We store likes as a collection of collections. + Root key is the url of my messages + Each object has a sub-collection of the specific message made by someone. + */ + const id = (message.id).toString(); + const objectId = (message.object).toString(); + const rootDocRef = collection.doc(objectId.replace(/\//g, "_")); + const rootDoc = await rootDocRef.get(); + + if (rootDoc.exists == false) { + console.log("Root doesn't exists, make it so."); + await rootDocRef.set({}); + } + + const messagesCollection = rootDocRef.collection('messages'); + const messageDocRef = messagesCollection.doc(id.replace(/\//g, "_")); + const messageDoc = await messageDocRef.get(); + + if (messageDoc.exists == false) { + console.log(`Adding message "${id}" to ${objectId}`); + await messageDocRef.set(message); + } +} + +async function saveAnnounce(message: AP.Announce) { + // If from Mastodon - someone boosted the post. + const collection = db.collection('announces'); + + // We should do some checks + // 1. TODO: in reply to is against a post that I made. + + console.log("Save Announce", message); + + /* + We store announces as a collection of collections. + Root key is the url of my messages + Each object has a sub-collection of the specific message made by someone. + */ + const id = (message.id).toString(); + const objectId = (message.object).toString(); + const rootDocRef = collection.doc(objectId.replace(/\//g, "_")); + const rootDoc = await rootDocRef.get(); + + if (rootDoc.exists == false) { + await rootDocRef.set({}); + } + + const messagesCollection = rootDocRef.collection('messages'); + const messageDocRef = messagesCollection.doc(id.replace(/\//g, "_")); + const messageDoc = await messageDocRef.get(); + + if (messageDoc.exists == false) { + console.log(`Adding message "${id}" to ${objectId}`); + await messageDocRef.set(message); + } +} + +async function saveReply(message: AP.Create) { + // If from Mastodon - someone boosted the post. + const collection = db.collection('replies'); + + if (message.object == undefined) return; + + // We should do some checks + // 1. TODO: in reply to is against a post that I made. + + console.log("Save Reply", message); + + /* + We store announces as a collection of collections. + Root key is the url of my messages + Each object has a sub-collection of the specific message made by someone. + */ + const objectId = ((message.object).inReplyTo).toString(); + const rootDocRef = collection.doc(objectId.replace(/\//g, "_")); + const rootDoc = await rootDocRef.get(); + + if (rootDoc.exists == false) { + await rootDocRef.set({}); + } + + const messagesCollection = rootDocRef.collection('messages'); + + const id = (message.id).toString(); + const messageDocRef = messagesCollection.doc(id.replace(/\//g, "_")); + const messageDoc = await messageDocRef.get(); + + if (messageDoc.exists == false) { + console.log(`Adding message "${id}" to ${objectId}`); + await messageDocRef.set(message); + } +} diff --git a/api/activitypub/outbox.ts b/api/activitypub/outbox.ts new file mode 100644 index 0000000..5ab151c --- /dev/null +++ b/api/activitypub/outbox.ts @@ -0,0 +1,20 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import { join } from 'path'; +import { cwd } from 'process'; +import { readFileSync } from 'fs'; + +/* + This returns a list of posts for the single user 'coder'. + + It's a GET request. This doesn't post it to anyone's timeline. +*/ +export default function (req: VercelRequest, res: VercelResponse) { + // All of the outbox data is generated at build time, so just return that static file. + const file = join(cwd(), 'public', 'outbox.ajson'); + const stringified = readFileSync(file, 'utf8'); + + res.statusCode = 200; + res.setHeader("Content-Type", `application/activity+json`); + + return res.end(stringified); +}; diff --git a/api/activitypub/sendNote.ts b/api/activitypub/sendNote.ts new file mode 100644 index 0000000..55b02dc --- /dev/null +++ b/api/activitypub/sendNote.ts @@ -0,0 +1,120 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import { AP } from 'activitypub-core-types'; +import admin from 'firebase-admin'; +import { OrderedCollection } from 'activitypub-core-types/lib/activitypub/index.js'; +import { sendSignedRequest } from '../../lib/activitypub/utils/sendSignedRequest.js'; +import { fetchActorInformation } from '../../lib/activitypub/utils/fetchActorInformation.js'; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +if (!admin.apps.length) { + admin.initializeApp({ + credential: admin.credential.cert({ + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + clientEmail: process.env.FIREBASE_CLIENT_EMAIL, + privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n') + }) + }); +} + +const db = admin.firestore(); + +/* + Sends the latest not that hasn't yet been sent. +*/ +export default async function (req: VercelRequest, res: VercelResponse) { + const { body, query, method, url, headers } = req; + + if ( + headers.authorization !== `Bearer ${process.env.CRON_SECRET}` + ) { + return res.status(401).end("Unauthorized"); + } + + const configCollection = db.collection('config'); + const configRef = configCollection.doc("config"); + const config = await configRef.get(); + + if (config.exists == false) { + // Config doesn't exist, make something + configRef.set({ + "lastId": "" + }); + } + + const configData = config.data(); + let lastId = ""; + if (configData != undefined) { + lastId = configData.lastId; + } + + // Get my outbox because it contains all my notes. + const outboxResponse = await fetch('https://coderrrrr.site/outbox'); + const outbox = (await outboxResponse.json()); + + const followersCollection = db.collection('followers'); + const followersQuerySnapshot = await followersCollection.get(); + + let lastSuccessfulSentId = ""; + + for (const followerDoc of followersQuerySnapshot.docs) { + const follower = followerDoc.data(); + try { + const actorUrl = (typeof follower.actor == "string") ? follower.actor : follower.actor.id; + console.log(`Fetching actor information for ${actorUrl}`) + const actorInformation = await fetchActorInformation(actorUrl); + if (actorInformation == undefined) { + // We can't send to this actor, so skip the actor. We should log it. + continue; + } + + if (actorInformation.inbox == undefined) { + console.log( + `Actor ${actorUrl} doesn't have an inbox, so we can't send to them. ${actorInformation}` + ); + } + const actorInbox = new URL(actorInformation.inbox.toString()); + + for (const iteIdx in (outbox.orderedItems)) { + // We have to break somewhere... do it after the first. + const item = (outbox.orderedItems)[iteIdx]; + + console.log(`Checking ID ${item.id}, ${lastId}`); + if (item.id == `${lastId}`) { + lastSuccessfulSentId = item.id; + // We've already posted this, don't try and send it again. + console.log(`${item.id} has already been posted - don't attempt`) + break; + } + + if (item.object != undefined) { + // We might not need this. + item.object.published = (new Date()).toISOString(); + } + + // Item will be an entity, i.e, { Create { Note } } + try { + console.log(`Sending to ${actorInbox}`); + + const response = await sendSignedRequest(actorInbox, item); + console.log(`Send result: ${actorInbox}`, response.status, response.statusText, await response.text()); + + // It's not been sent. + lastSuccessfulSentId = item.id; // we shouldn't really set this every time. + } catch (sendSignedError) { + console.log("Error sending signed request", sendSignedError) + } + + break; // At some point we might want to post more than one post, so remove this. + } + } catch (ex) { + console.log("Error", ex); + } + } + + configRef.set({ + "lastId": lastSuccessfulSentId + }); + + res.status(200).end("ok"); +}; diff --git a/api/well-known/webfinger.ts b/api/well-known/webfinger.ts new file mode 100644 index 0000000..774bd11 --- /dev/null +++ b/api/well-known/webfinger.ts @@ -0,0 +1,20 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +export default function (req: VercelRequest, res: VercelResponse) { + const { resource } = req.query; + res.statusCode = 200; + res.setHeader("Content-Type", `application/jrd+json`); + res.end(`{ + "subject": "acct:coder@coderrrrr.site", + "aliases": [ + "https://coderrrrr.site/@coder" + ], + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": "https://coderrrrr.site/coder" + } + ] + }`); +} diff --git a/netlify.toml.old b/netlify.toml.old deleted file mode 100644 index b20a6b5..0000000 --- a/netlify.toml.old +++ /dev/null @@ -1,8 +0,0 @@ -[[redirects]] - from = "/.well-known/host-meta*" - to = "https://fed.brid.gy/.well-known/host-meta:splat" - status = 302 -[[redirects]] - from = "/.well-known/webfinger*" - to = "https://fed.brid.gy/.well-known/webfinger" - status = 302 diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..ab4e997 --- /dev/null +++ b/vercel.json @@ -0,0 +1,66 @@ +{ + "functions": { + "api/**/*": { + "maxDuration": 240 + } + }, + "crons": [{ + "path": "/api/activitypub/sendNote.ts", + "schedule": "0 * * * *" + }], + "redirects": [ + { + "source": "/amp/(.+)/", + "destination": "/$1/" + }, + { + "source": "/(\\d+)/(\\d+)/(.+).html", + "destination": "/$3/" + } + ], + "rewrites": [ + { + "source": "/(.+)/$", + "destination": "/$1/index.html" + }, + { + "source": "/.well-known/(.*)", + "destination": "/api/well-known/$1" + }, + { + "source": "/authorize_interaction", + "destination": "/api/activitypub/authorize_interaction.ts" + }, + { + "source": "/coder", + "destination": "/api/activitypub/actor.js" + }, + { + "source": "/followers", + "destination": "/api/activitypub/followers.js" + }, + { + "source": "/following", + "destination": "/api/activitypub/following.js" + }, + { + "source": "/inbox", + "destination": "/api/activitypub/inbox.js" + }, + { + "source": "/outbox", + "destination": "/api/activitypub/outbox.js" + } + ], + "headers": [ + { + "source": "/(.*).ajson", + "headers": [ + { + "key": "content-type", + "value": "application/activity+json" + } + ] + } + ] +}