mirror of
https://github.com/turbomaster95/coderrrrr.git
synced 2025-08-02 10:42:24 +00:00
Compare commits
No commits in common. "0995069a74f2b844e522e749c3bf622742e504fe" and "fe42562591600b9b15a86c932e7bc5f8737f8ace" have entirely different histories.
0995069a74
...
fe42562591
177
.gitignore
vendored
177
.gitignore
vendored
@ -1,177 +0,0 @@
|
|||||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
|
|
||||||
logs
|
|
||||||
_.log
|
|
||||||
npm-debug.log_
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Caches
|
|
||||||
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
|
|
||||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
|
|
||||||
pids
|
|
||||||
_.pid
|
|
||||||
_.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
|
|
||||||
.temp
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
# IntelliJ based IDEs
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Finder (MacOS) folder config
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
bun.lockb
|
|
37
.well-known/webfinger
Normal file
37
.well-known/webfinger
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
1
@coder.json
Normal file
1
@coder.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"@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"}}
|
@ -1,39 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
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');
|
|
||||||
};
|
|
@ -1,34 +0,0 @@
|
|||||||
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) {
|
|
||||||
try {
|
|
||||||
const collection = db.collection('followers');
|
|
||||||
const actors = await collection.select("actor").get();
|
|
||||||
|
|
||||||
const output = {
|
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
|
||||||
"id": "https://coderrrrr.site/api/activitypub/followers",
|
|
||||||
"type": "OrderedCollection",
|
|
||||||
"totalItems": actors.docs.length,
|
|
||||||
"orderedItems": actors.docs.map(item => item.get("actor"))
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).setHeader("Content-Type", "application/activity+json").json(output);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching followers:", error);
|
|
||||||
res.status(500).json({ error: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,16 +0,0 @@
|
|||||||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
||||||
|
|
||||||
export default function (req: VercelRequest, res: VercelResponse) {
|
|
||||||
const output = {
|
|
||||||
"@context": "https://www.w3.org/ns/activitystreams",
|
|
||||||
"id": "https://coderrrrr.site/api/activitypub/following",
|
|
||||||
"type": "OrderedCollection",
|
|
||||||
"totalItems": 2,
|
|
||||||
"orderedItems": [
|
|
||||||
"https://mastodon.social/users/coderrrrr",
|
|
||||||
"https://usr.cloud/users/coder"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).setHeader("Content-Type", "application/activity+json").json(output);
|
|
||||||
};
|
|
@ -1,347 +0,0 @@
|
|||||||
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 = <AP.Activity>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(<AP.Follow>message, actorInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type == "Like") {
|
|
||||||
await saveLike(<AP.Like>message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type == "Announce") {
|
|
||||||
await saveAnnounce(<AP.Announce>message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type == "Create") {
|
|
||||||
console.log("Message type Create")
|
|
||||||
// Someone is sending us a message.
|
|
||||||
const createMessage = <AP.Create>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 = <CoreObject>createMessage.object
|
|
||||||
if (createObject.type == "Note" && createObject.inReplyTo != undefined) {
|
|
||||||
await saveReply(<AP.Create>createMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type == "Undo") {
|
|
||||||
// Undo a follow.
|
|
||||||
const undoObject: AP.Undo = <AP.Undo>message;
|
|
||||||
if (undoObject == null || undoObject.id == null) return;
|
|
||||||
if (undoObject.object == null) return;
|
|
||||||
if ("actor" in undoObject.object == false) return;
|
|
||||||
|
|
||||||
if ((<CoreObject>undoObject.object).type == "Follow") {
|
|
||||||
await removeFollow(<AP.Follow>undoObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((<CoreObject>undoObject.object).type == "Like") {
|
|
||||||
await removeLike(<AP.Like>undoObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((<CoreObject>undoObject.object).type == "Announce") {
|
|
||||||
await removeAnnounce(<AP.Announce>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(<EntityReference>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(<EntityReference>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 = <AP.Accept><unknown>{
|
|
||||||
"@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(<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 = (<URL>message.id).toString();
|
|
||||||
const objectId = (<URL>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 = (<URL>message.id).toString();
|
|
||||||
const objectId = (<URL>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 = (<URL>(<CoreObject>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 = (<URL>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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
@ -1,120 +0,0 @@
|
|||||||
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 = <OrderedCollection>(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 (<AP.EntityReference[]>outbox.orderedItems)) {
|
|
||||||
// We have to break somewhere... do it after the first.
|
|
||||||
const item = (<AP.EntityReference[]>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, <AP.Activity> 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");
|
|
||||||
};
|
|
@ -1,35 +0,0 @@
|
|||||||
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",
|
|
||||||
"https://coderrrrr.site/@coder"
|
|
||||||
],
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"rel": "http://webfinger.net/rel/profile-page",
|
|
||||||
"type": "text/html",
|
|
||||||
"href": "https://coderrrrr.site/coder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rel": "self",
|
|
||||||
"type": "application/activity+json",
|
|
||||||
"href": "https://coderrrrr.site/coder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
|
||||||
"template": "https://coderrrrr.site/authorize_interaction?uri={uri}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rel": "http://webfinger.net/rel/avatar",
|
|
||||||
"type": "image/png",
|
|
||||||
"href": "https://files.usr.cloud/v1/AUTH_f22cbcf5b3904990be9696691ff73fc6/files.usr.cloud/accounts/avatars/113/822/701/816/479/941/original/7d7f6088ba1ddd57.png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`);
|
|
||||||
}
|
|
@ -7,15 +7,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>coderrrrr</title>
|
<title>coderrrrr</title>
|
||||||
<link href="https://github.com/turbomaster95" rel="me"></a>
|
<link href="https://github.com/turbomaster95" rel="me"></a>
|
||||||
<!-- <link href="https://usr.cloud/@coder" rel="me"> -->
|
<link href="https://usr.cloud/@coder" rel="me">
|
||||||
<link rel="webmention" href="https://webmention.io/coderrrrr.site/webmention" />
|
<link rel="webmention" href="https://webmention.io/coderrrrr.site/webmention" />
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="https://blog.coderrrrr.site/index.xml" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="https://blog.coderrrrr.site/index.xml" />
|
||||||
<link rel="openid.delegate" href="https://coderrrrr.site/"/>
|
|
||||||
<link rel="openid.server" href="https://indieauth.com/openid"/>
|
|
||||||
<link rel="authorization_endpoint" href="https://indieauth.com/auth"/>
|
|
||||||
<link rel="token_endpoint" href="https://tokens.indieauth.com/token"/>
|
|
||||||
<link href="https://coderrrrr.site/coder" rel="alternate" type="application/activity+json">
|
|
||||||
<link rel="webfinger" href="https://coderrrrr.site/.well-known/webfinger">
|
|
||||||
<link href="indeedlayout.css" rel="stylesheet" type="text/css" media="all">
|
<link href="indeedlayout.css" rel="stylesheet" type="text/css" media="all">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { AP } from "activitypub-core-types";
|
|
||||||
|
|
||||||
export async function fetchActorInformation(actorUrl: string): Promise<AP.Actor | null> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
actorUrl,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-type": 'application/activity+json',
|
|
||||||
"Accept": 'application/activity+json'
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(5000) // kill after 5 seconds
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Unable to fetch action information", actorUrl);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { VercelRequest } from '@vercel/node';
|
|
||||||
import parser from '../../http-signature/index.js';
|
|
||||||
|
|
||||||
export function parseSignature(request: VercelRequest) {
|
|
||||||
const { url, method, headers } = request;
|
|
||||||
return parser.parse({ url, method, headers });
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import { AP } from 'activitypub-core-types';
|
|
||||||
import { Sha256Signer } from '../../http-signature/index.js';
|
|
||||||
import { createHash } from 'crypto';
|
|
||||||
|
|
||||||
export async function sendSignedRequest(endpoint: URL, object: AP.Activity): Promise<Response> {
|
|
||||||
const publicKeyId = "https://coderrrrr.site/coder#main-key";
|
|
||||||
const privateKey = process.env.ACTIVITYPUB_PRIVATE_KEY;
|
|
||||||
|
|
||||||
const signer = new Sha256Signer({ publicKeyId, privateKey, headerNames: ["host", "date", "digest"] });
|
|
||||||
|
|
||||||
const requestHeaders = {
|
|
||||||
host: endpoint.hostname,
|
|
||||||
date: new Date().toUTCString(),
|
|
||||||
digest: `SHA-256=${createHash('sha256').update(JSON.stringify(object)).digest('base64')}`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate the signature header
|
|
||||||
const signature = signer.sign({
|
|
||||||
url: endpoint,
|
|
||||||
method: "POST",
|
|
||||||
headers: requestHeaders
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("object", object);
|
|
||||||
console.log("endpoint", endpoint);
|
|
||||||
console.log("requestHeaders", requestHeaders);
|
|
||||||
console.log("signature", signature);
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
endpoint,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(object),
|
|
||||||
headers: {
|
|
||||||
'content-type': "application/activity+json",
|
|
||||||
accept: "application/activity+json",
|
|
||||||
...requestHeaders,
|
|
||||||
signature: signature
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(5000) // kill after 5 seconds
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
export function verifySignature(signature:any, publicKeyJson:any) {
|
|
||||||
let signatureValid;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Verify the signature
|
|
||||||
signatureValid = signature.verify(
|
|
||||||
publicKeyJson.publicKeyPem
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Signature Verification error", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return signatureValid;
|
|
||||||
}
|
|
@ -1,215 +0,0 @@
|
|||||||
/** PK Fixed version.
|
|
||||||
* Activitypub HTTP Signatures
|
|
||||||
* Based on [HTTP Signatures draft 08](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08)
|
|
||||||
* @module activitypub-http-signatures
|
|
||||||
*/
|
|
||||||
|
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
|
|
||||||
// token definitions from definitions in rfc7230 and rfc7235
|
|
||||||
const token = String.raw`[!#$%&'\*+\-\.\^_\`\|~0-9a-z]+`; // Key or value
|
|
||||||
const qdtext = String.raw`[^"\\\x7F]`; // Characters that don't need escaping
|
|
||||||
const quotedPair = String.raw`\\[\t \x21-\x7E\x80-\xFF]`; // Escaped characters
|
|
||||||
const quotedString = `(?:${qdtext}|${quotedPair})*`;
|
|
||||||
const fieldMatch = new RegExp(String.raw`(?<=^|,\s*)(${token})\s*=\s*(?:(${token})|"(${quotedString})")(?=,|$)`, 'ig');
|
|
||||||
const parseSigFields = str => Object.fromEntries(
|
|
||||||
Array.from(
|
|
||||||
str.matchAll(fieldMatch)
|
|
||||||
).map(
|
|
||||||
// capture groups: 1=fieldname, 2=field value if unquoted, 3=field value if quoted
|
|
||||||
v=>[
|
|
||||||
v[1],
|
|
||||||
v[2] ?? v[3].replace(
|
|
||||||
/\\./g,
|
|
||||||
c=>c[1]
|
|
||||||
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultHeaderNames = ['(request-target)', 'host', 'date'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Generate the string to be signed for the signature header
|
|
||||||
* @param {Object} options Options
|
|
||||||
* @param {string} options.target The pathname of the request URL (including query and hash strings)
|
|
||||||
* @param {string} options.method The HTTP request method
|
|
||||||
* @param {object} options.headers Object whose keys are http header names and whose values are those headers' values
|
|
||||||
* @param {string[]} headerNames Names of the headers to use in the signature
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getSignString({ target, method, headers }, headerNames) {
|
|
||||||
const requestTarget = `${method.toLowerCase()} ${target}`;
|
|
||||||
headers = {
|
|
||||||
...headers,
|
|
||||||
'(request-target)': requestTarget
|
|
||||||
};
|
|
||||||
return headerNames.map(header => `${header.toLowerCase()}: ${headers[header]}`).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Sha256Signer {
|
|
||||||
#publicKeyId;
|
|
||||||
#privateKey;
|
|
||||||
#headerNames;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for signing a request and returning the signature header
|
|
||||||
* @param {object} options Config options
|
|
||||||
* @param {string} options.publicKeyId URI for public key that must be used for verification
|
|
||||||
* @param {string} options.privateKey Private key to use for signing
|
|
||||||
* @param {string[]} options.headerNames Names of headers to use in generating signature
|
|
||||||
*/
|
|
||||||
constructor({ publicKeyId, privateKey, headerNames }) {
|
|
||||||
this.#publicKeyId = publicKeyId;
|
|
||||||
this.#privateKey = privateKey;
|
|
||||||
this.#headerNames = headerNames ?? defaultHeaderNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate the signature header for an outgoing message
|
|
||||||
* @param {object} reqOptions Request options
|
|
||||||
* @param {string} reqOptions.url The full URL of the request to sign
|
|
||||||
* @param {string} reqOptions.method Method of the request
|
|
||||||
* @param {object} reqOptions.headers Dict of headers used in the request
|
|
||||||
* @returns {string} Value for the signature header
|
|
||||||
*/
|
|
||||||
sign({ url, method, headers }) {
|
|
||||||
const { host, pathname, search } = new URL(url);
|
|
||||||
const target = `${pathname}${search}`;
|
|
||||||
headers.date = headers.date || new Date().toUTCString();
|
|
||||||
headers.host = headers.host || host;
|
|
||||||
|
|
||||||
const headerNames = this.#headerNames;
|
|
||||||
|
|
||||||
const stringToSign = getSignString({ target, method, headers }, headerNames);
|
|
||||||
|
|
||||||
const signature = this.#signSha256(this.#privateKey, stringToSign).toString('base64');
|
|
||||||
|
|
||||||
return `keyId="${this.#publicKeyId}",headers="${headerNames.join(' ')}",signature="${signature.replace(/"/g, '\\"')}",algorithm="rsa-sha256"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Sign a string with a private key using sha256 alg
|
|
||||||
* @param {string} privateKey Private key
|
|
||||||
* @param {string} stringToSign String to sign
|
|
||||||
* @returns {Buffer} Signature buffer
|
|
||||||
*/
|
|
||||||
#signSha256(privateKey, stringToSign) {
|
|
||||||
const signer = crypto.createSign('sha256');
|
|
||||||
signer.update(stringToSign);
|
|
||||||
const signature = signer.sign(privateKey);
|
|
||||||
signer.end();
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Incoming request parser and Signature factory.
|
|
||||||
* If you wish to support more signature types you can extend this class
|
|
||||||
* and overide getSignatureClass.
|
|
||||||
*/
|
|
||||||
export class Parser {
|
|
||||||
/**
|
|
||||||
* Parse an incomming request's http signature header
|
|
||||||
* @param {object} reqOptions Request options
|
|
||||||
* @param {string} reqOptions.url The pathname (and query string) of the request URL
|
|
||||||
* @param {string} reqOptions.method Method of the request
|
|
||||||
* @param {object} reqOptions.headers Dict of headers used in the request
|
|
||||||
* @returns {Signature} Object representing the signature
|
|
||||||
* @throws {UnkownAlgorithmError} If the algorithm used isn't one we know how to verify
|
|
||||||
*/
|
|
||||||
parse({ headers, method, url }){
|
|
||||||
const fields = parseSigFields(headers.signature);
|
|
||||||
const headerNames = (fields.headers ?? 'date').split(/\s+/);
|
|
||||||
const signature = Buffer.from(fields.signature, 'base64');
|
|
||||||
const signString = getSignString({ target: url, method, headers }, headerNames);
|
|
||||||
const keyId = fields.keyId;
|
|
||||||
const algorithm = fields.algorithm ?? 'rsa-sha256';
|
|
||||||
|
|
||||||
return this.getSignatureClass(algorithm, { signature, string: signString, keyId });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct the signature class for a given algorithm.
|
|
||||||
* Override this method if you want to support additional
|
|
||||||
* algorithms.
|
|
||||||
* @param {string} algorithm The algorithm used by the signed request
|
|
||||||
* @param {object} options
|
|
||||||
* @param {Buffer} options.signature The signature as a buffer
|
|
||||||
* @param {string} options.string The string that was signed
|
|
||||||
* @param {string} options.keyId The ID of the public key to be used for verification
|
|
||||||
* @returns {Signature}
|
|
||||||
* @throws {UnkownAlgorithmError} If an unknown algorithm was used
|
|
||||||
*/
|
|
||||||
getSignatureClass(algorithm, { signature, string, keyId }) {
|
|
||||||
if(algorithm === 'rsa-sha256') {
|
|
||||||
return new Sha256Signature({ signature, string, keyId });
|
|
||||||
} else {
|
|
||||||
throw new UnkownAlgorithmError(`Don't know how to verify ${algorithm} signatures.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UnkownAlgorithmError extends Error {}
|
|
||||||
|
|
||||||
export class Signature {
|
|
||||||
#keyId;
|
|
||||||
|
|
||||||
constructor(keyId) {
|
|
||||||
this.#keyId = keyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get keyId(){
|
|
||||||
return this.#keyId;
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(key){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Sha256Signature extends Signature {
|
|
||||||
#signature;
|
|
||||||
#string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class representing the HTTP signature
|
|
||||||
* @param {object} options
|
|
||||||
* @param {Buffer} options.signature The signature as a buffer
|
|
||||||
* @param {string} options.string The string that was signed
|
|
||||||
* @param {string} options.keyId The ID of the public key to be used for verification
|
|
||||||
*/
|
|
||||||
constructor({ signature, string, keyId }) {
|
|
||||||
super(keyId);
|
|
||||||
this.#signature = signature;
|
|
||||||
this.#string = string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property {string} keyId The ID of the public key that can verify the signature
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify the signature using a public key
|
|
||||||
* @param {string} key The public key matching the signature's keyId
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
verify(key) {
|
|
||||||
const signature = this.#signature;
|
|
||||||
const signedString = this.#string;
|
|
||||||
const verifier = crypto.createVerify('sha256');
|
|
||||||
verifier.write(signedString);
|
|
||||||
verifier.end();
|
|
||||||
|
|
||||||
return verifier.verify(key, signature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default export: new instance of Parser class
|
|
||||||
*/
|
|
||||||
export default new Parser;
|
|
8
netlify.toml.old
Normal file
8
netlify.toml.old
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[[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
|
29
package.json
29
package.json
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "coderrrrr.site",
|
|
||||||
"type": "module",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "latest"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.7.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@firebase/app-types": "^0.9.3",
|
|
||||||
"@types/multiparty": "^4.2.1",
|
|
||||||
"@types/node": "^22.13.1",
|
|
||||||
"@types/uuid": "^10.0.0",
|
|
||||||
"@vercel/analytics": "^1.4.1",
|
|
||||||
"@vercel/edge": "^1.2.1",
|
|
||||||
"@vercel/kv": "^3.0.0",
|
|
||||||
"@vercel/node": "^5.1.2",
|
|
||||||
"@vercel/og": "^0.6.5",
|
|
||||||
"activitypub-core-types": "^0.3.2",
|
|
||||||
"ava": "^6.2.0",
|
|
||||||
"file-drop-element": "^1.0.1",
|
|
||||||
"firebase-admin": "^13.1.0",
|
|
||||||
"multiparty": "^4.2.3",
|
|
||||||
"node-fetch": "^3.3.2",
|
|
||||||
"uuid": "^11.0.5",
|
|
||||||
"vercel": "^41.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhSF4YYoESfQABVsOoO2x
|
|
||||||
xDyrK0zytFGhOr2aRHpmIcY//Dr5BhbJegHGGAVQSubHs+mS8YwZ/1NxcGuH870h
|
|
||||||
wTFKrVUJaMYf94WXNGKpieOBL8BAkqqAPwFL7KCto2FAwjjbstOfxIQULvH1qnlo
|
|
||||||
ic10/pKA8ide0q7X91qKmN7IYmFUmRCph7hWDJHoWsxdNaP8DS4/P4IRPfMHiUt3
|
|
||||||
zzTKirM+6yjh7bjca6FbEmR8dDNqkrTwC5jlst32ZXE0Ncfw8tSYhW4tEJR9DyAm
|
|
||||||
9t1XBbpwPV3aWcnIfUfHYxXqShyp7rHqUQvpRsnfuCUklTwgxdBEQ/oxSgwcQ33z
|
|
||||||
nwIDAQAB
|
|
||||||
-----END PUBLIC KEY-----
|
|
66
vercel.json
66
vercel.json
@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"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.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "/followers",
|
|
||||||
"destination": "/api/activitypub/followers.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "/following",
|
|
||||||
"destination": "/api/activitypub/following.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "/inbox",
|
|
||||||
"destination": "/api/activitypub/inbox.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": "/outbox",
|
|
||||||
"destination": "/api/activitypub/outbox.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"source": "/(.*).ajson",
|
|
||||||
"headers": [
|
|
||||||
{
|
|
||||||
"key": "content-type",
|
|
||||||
"value": "application/activity+json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user