mirror of
				https://github.com/turbomaster95/coderrrrr.git
				synced 2025-11-04 09:18:35 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			fe42562591
			...
			0995069a74
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0995069a74 | |||
| c57f30d45e | |||
| d5b415f031 | |||
| cec716472e | |||
| 831a5414e8 | |||
| 1f062c18d8 | |||
| 99fa97e792 | |||
| 770953d83f | |||
| d1db7dbfb1 | |||
| b5327aebc7 | |||
| 4a5ac147b0 | |||
| 55f5278730 | |||
| ecf9fe1dc2 | |||
| 74c1dc8f94 | |||
| ccb5e16eef | |||
| ffece8bf84 | |||
| f3de01680a | |||
| 54a7d8f235 | 
							
								
								
									
										177
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,177 @@
 | 
				
			|||||||
 | 
					# 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
 | 
				
			||||||
@ -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"
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -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"}}
 | 
					 | 
				
			||||||
							
								
								
									
										39
									
								
								api/activitypub/actor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								api/activitypub/actor.ts
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								api/activitypub/authorize_interaction.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/activitypub/authorize_interaction.ts
									
									
									
									
									
										Normal file
									
								
							@ -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');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										34
									
								
								api/activitypub/followers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								api/activitypub/followers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					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" });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										16
									
								
								api/activitypub/following.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								api/activitypub/following.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					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);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										347
									
								
								api/activitypub/inbox.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								api/activitypub/inbox.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 = <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);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								api/activitypub/outbox.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/activitypub/outbox.ts
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										120
									
								
								api/activitypub/sendNote.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								api/activitypub/sendNote.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 = <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");
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										35
									
								
								api/well-known/webfinger.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								api/well-known/webfinger.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					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,9 +7,15 @@
 | 
				
			|||||||
    <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										21
									
								
								lib/activitypub/utils/fetchActorInformation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								lib/activitypub/utils/fetchActorInformation.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										7
									
								
								lib/activitypub/utils/parseSignature.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								lib/activitypub/utils/parseSignature.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					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 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										45
									
								
								lib/activitypub/utils/sendSignedRequest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/activitypub/utils/sendSignedRequest.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								lib/activitypub/utils/verifySignature.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								lib/activitypub/utils/verifySignature.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										215
									
								
								lib/http-signature/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										215
									
								
								lib/http-signature/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,215 @@
 | 
				
			|||||||
 | 
					/** 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;
 | 
				
			||||||
@ -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
 | 
					 | 
				
			||||||
							
								
								
									
										29
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "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"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								pubkey.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pubkey.pem
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					-----BEGIN PUBLIC KEY-----
 | 
				
			||||||
 | 
					MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhSF4YYoESfQABVsOoO2x
 | 
				
			||||||
 | 
					xDyrK0zytFGhOr2aRHpmIcY//Dr5BhbJegHGGAVQSubHs+mS8YwZ/1NxcGuH870h
 | 
				
			||||||
 | 
					wTFKrVUJaMYf94WXNGKpieOBL8BAkqqAPwFL7KCto2FAwjjbstOfxIQULvH1qnlo
 | 
				
			||||||
 | 
					ic10/pKA8ide0q7X91qKmN7IYmFUmRCph7hWDJHoWsxdNaP8DS4/P4IRPfMHiUt3
 | 
				
			||||||
 | 
					zzTKirM+6yjh7bjca6FbEmR8dDNqkrTwC5jlst32ZXE0Ncfw8tSYhW4tEJR9DyAm
 | 
				
			||||||
 | 
					9t1XBbpwPV3aWcnIfUfHYxXqShyp7rHqUQvpRsnfuCUklTwgxdBEQ/oxSgwcQ33z
 | 
				
			||||||
 | 
					nwIDAQAB
 | 
				
			||||||
 | 
					-----END PUBLIC KEY-----
 | 
				
			||||||
							
								
								
									
										66
									
								
								vercel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								vercel.json
									
									
									
									
									
										Normal file
									
								
							@ -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.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