mirror of
				https://github.com/turbomaster95/coderrrrr.git
				synced 2025-11-04 09:18:35 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			31b776e956
			...
			faaef129e8
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| faaef129e8 | |||
| d5aa69ba98 | |||
| a76ad935ba | |||
| 1e038326a8 | 
@ -4,7 +4,7 @@ export default async function (req: VercelRequest, res: VercelResponse) {
 | 
				
			|||||||
  const { headers } = req;
 | 
					  const { headers } = req;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (headers.accept && headers.accept.includes("text/html")) {
 | 
					  if (headers.accept && headers.accept.includes("text/html")) {
 | 
				
			||||||
    res.redirect(302, "https://coderrrrr.site/");
 | 
					    res.redirect(302, "https://coder.is-a.dev/");
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ export default function (req: VercelRequest, res: VercelResponse) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Expected format: acct:username@coderrrrr.site
 | 
					  // Expected format: acct:username@coderrrrr.site
 | 
				
			||||||
  const match = resource.match(/^acct:([^@]+)@coderrrrr\.site$/i);
 | 
					  const match = resource.match(/^acct:([^@]+)@coder\.is-a.dev$/i);
 | 
				
			||||||
  if (!match) {
 | 
					  if (!match) {
 | 
				
			||||||
    res.status(400).json({ error: "Resource format not supported" });
 | 
					    res.status(400).json({ error: "Resource format not supported" });
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
@ -22,7 +22,7 @@ export default function (req: VercelRequest, res: VercelResponse) {
 | 
				
			|||||||
  // You can add more supported usernames as needed.
 | 
					  // You can add more supported usernames as needed.
 | 
				
			||||||
  let profileUrl: string;
 | 
					  let profileUrl: string;
 | 
				
			||||||
  if (username === "blog") {
 | 
					  if (username === "blog") {
 | 
				
			||||||
    profileUrl = "https://coderrrrr.site/@blog";
 | 
					    profileUrl = "https://coder.is-a.dev/@blog";
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // If the username is not recognized, return a 404.
 | 
					    // If the username is not recognized, return a 404.
 | 
				
			||||||
    res.status(404).json({ error: "User not found" });
 | 
					    res.status(404).json({ error: "User not found" });
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										74
									
								
								netlify.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								netlify.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					[build]
 | 
				
			||||||
 | 
					functions = "netlify/functions"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Redirects from old AMP and HTML URLs
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/amp/:slug/"
 | 
				
			||||||
 | 
					to = "/:slug/"
 | 
				
			||||||
 | 
					status = 301
 | 
				
			||||||
 | 
					force = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/:year/:month/:slug.html"
 | 
				
			||||||
 | 
					to = "/:slug/"
 | 
				
			||||||
 | 
					status = 301
 | 
				
			||||||
 | 
					force = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Rewrites for ActivityPub API endpoints
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/.well-known/*"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/well-known/:splat"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/authorize_interaction"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/authorize_interaction"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/@blog"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/actor"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/coder"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/old"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/followers"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/followers"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/following"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/following"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/inbox"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/inbox"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/sharedInbox"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/sharedInbox"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/outbox"
 | 
				
			||||||
 | 
					to = "/.netlify/functions/outbox"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Rewrite for trailing slashes to index.html
 | 
				
			||||||
 | 
					[[redirects]]
 | 
				
			||||||
 | 
					from = "/:path/"
 | 
				
			||||||
 | 
					to = "/:path/index.html"
 | 
				
			||||||
 | 
					status = 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Custom content-type for ActivityPub JSON
 | 
				
			||||||
 | 
					[[headers]]
 | 
				
			||||||
 | 
					for = "/*.json"
 | 
				
			||||||
 | 
					  [headers.values]
 | 
				
			||||||
 | 
					  Content-Type = "application/activity+json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										71
									
								
								netlify/functions/actor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								netlify/functions/actor.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					exports.handler = async (event, context) => {
 | 
				
			||||||
 | 
					  const headers = event.headers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (headers.accept && headers.accept.includes("text/html")) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 302,
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        Location: "https://coder.is-a.dev/"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    statusCode: 200,
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/activity+json"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: JSON.stringify({
 | 
				
			||||||
 | 
					      "@context": ["https://www.w3.org/ns/activitystreams", { "@language": "en-US" }],
 | 
				
			||||||
 | 
					      "type": "Person",
 | 
				
			||||||
 | 
					      "id": "https://coder.is-a.dev/@blog",
 | 
				
			||||||
 | 
					      "outbox": "https://coder.is-a.dev/api/activitypub/outbox",
 | 
				
			||||||
 | 
					      "following": "https://coder.is-a.dev/api/activitypub/following",
 | 
				
			||||||
 | 
					      "followers": "https://coder.is-a.dev/api/activitypub/followers",
 | 
				
			||||||
 | 
					      "sharedInbox": "https://coder.is-a.dev/api/activitypub/sharedInbox",
 | 
				
			||||||
 | 
					      "inbox": "https://coder.is-a.dev/api/activitypub/inbox",
 | 
				
			||||||
 | 
					      "url": "https://coder.is-a.dev/@blog",
 | 
				
			||||||
 | 
					      "published": "2024-05-02T15:25:40Z",
 | 
				
			||||||
 | 
					      "preferredUsername": "blog",
 | 
				
			||||||
 | 
					      "name": "Deva Midhun's blog",
 | 
				
			||||||
 | 
					      "manuallyApprovesFollowers": false,
 | 
				
			||||||
 | 
					      "discoverable": true,
 | 
				
			||||||
 | 
					      "indexable": true,
 | 
				
			||||||
 | 
					      "memorial": false,
 | 
				
			||||||
 | 
					      "summary": "Software developer & self-hosting enthusiast. This is a bridge between my blog and the fediverse!",
 | 
				
			||||||
 | 
					      "tag": [],
 | 
				
			||||||
 | 
					      "icon": {
 | 
				
			||||||
 | 
					        "type": "Image",
 | 
				
			||||||
 | 
					        "mediaType": "image/png",
 | 
				
			||||||
 | 
					        "url": "https://i.ibb.co/N6J5b8WS/download20250102015611.png"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "publicKey": {
 | 
				
			||||||
 | 
					        "id": "https://coder.is-a.dev/@blog#main-key",
 | 
				
			||||||
 | 
					        "owner": "https://coder.is-a.dev/@blog",
 | 
				
			||||||
 | 
					        "publicKeyPem": process.env.ACTIVITYPUB_PUBLIC_KEY || "MISSING_PUBLIC_KEY"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "attachment": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "PropertyValue",
 | 
				
			||||||
 | 
					          "value": "<a href=\"https://coder.is-a.dev\" target=\"_blank\" rel=\"nofollow noopener me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">coder.is-a.dev</span><span class=\"invisible\"></span></a>",
 | 
				
			||||||
 | 
					          "name": "Website"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "PropertyValue",
 | 
				
			||||||
 | 
					          "value": "<a href=\"https://github.com/turbomaster95\" target=\"_blank\" rel=\"nofollow noopener me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">github.com/turbomaster95</span><span class=\"invisible\"></span></a>",
 | 
				
			||||||
 | 
					          "name": "GitHub"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "PropertyValue",
 | 
				
			||||||
 | 
					          "value": "<a href=\"https://usr.cloud/@coder\" target=\"_blank\" rel=\"nofollow noopener me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">usr.cloud/@coder</span><span class=\"invisible\"></span></a>",
 | 
				
			||||||
 | 
					          "name": "Owner"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          "type": "PropertyValue",
 | 
				
			||||||
 | 
					          "value": "<a href=\"https://keyoxide.org/6389542B98CB868DAC73A373ED1190B780583CF6\" target=\"_blank\" rel=\"nofollow noopener me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">keyoxide.org/6389542B98CB868DA</span><span class=\"invisible\">C73A373ED1190B780583CF6</span></a>",
 | 
				
			||||||
 | 
					          "name": "Keyoxide"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										9
									
								
								netlify/functions/authorize_interaction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								netlify/functions/authorize_interaction.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					exports.handler = async (event, context) => {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    statusCode: 200,
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/jrd+json"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: "ok"
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										43
									
								
								netlify/functions/followers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								netlify/functions/followers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					import { Handler } from '@netlify/functions';
 | 
				
			||||||
 | 
					import { firestore } from '../../../lib/firebase';
 | 
				
			||||||
 | 
					import { fetchActorInformation } from '../../../lib/activitypub/utils/fetchActorInformation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handler: Handler = async (event, context) => {
 | 
				
			||||||
 | 
					  const [, , username] = event.path.split('/'); // /users/:username/followers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const followersSnapshot = await firestore
 | 
				
			||||||
 | 
					      .collection('followers')
 | 
				
			||||||
 | 
					      .where('following', '==', username)
 | 
				
			||||||
 | 
					      .get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const followers = followersSnapshot.docs.map(doc => ({
 | 
				
			||||||
 | 
					      type: 'Link',
 | 
				
			||||||
 | 
					      href: doc.data().follower,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const actor = await fetchActorInformation(username);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 200,
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        'Content-Type': 'application/activity+json',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      body: JSON.stringify({
 | 
				
			||||||
 | 
					        '@context': 'https://www.w3.org/ns/activitystreams',
 | 
				
			||||||
 | 
					        id: `${actor.id}/followers`,
 | 
				
			||||||
 | 
					        type: 'OrderedCollection',
 | 
				
			||||||
 | 
					        totalItems: followers.length,
 | 
				
			||||||
 | 
					        orderedItems: followers,
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 500,
 | 
				
			||||||
 | 
					      body: JSON.stringify({ error: error.message }),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { handler };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										24
									
								
								netlify/functions/following.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								netlify/functions/following.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					import { Handler } from '@netlify/functions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handler: Handler = async (event, context) => {
 | 
				
			||||||
 | 
					  const output = {
 | 
				
			||||||
 | 
					    "@context": "https://www.w3.org/ns/activitystreams",
 | 
				
			||||||
 | 
					    "id": "https://coder.is-a.dev/api/activitypub/following",
 | 
				
			||||||
 | 
					    "type": "OrderedCollection",
 | 
				
			||||||
 | 
					    "totalItems": 2,
 | 
				
			||||||
 | 
					    "orderedItems": [
 | 
				
			||||||
 | 
					      "https://mastodon.social/users/coderrrrr",
 | 
				
			||||||
 | 
					      "https://usr.cloud/users/coder"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    statusCode: 200,
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/activity+json",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: JSON.stringify(output),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { handler };
 | 
				
			||||||
							
								
								
									
										46
									
								
								netlify/functions/inbox.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								netlify/functions/inbox.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import { Handler } from '@netlify/functions';
 | 
				
			||||||
 | 
					import { firestore } from '../../../lib/firebase';
 | 
				
			||||||
 | 
					import { verifySignature } from '../../../lib/activitypub/utils/verifySignature.js';
 | 
				
			||||||
 | 
					import { handleFollow } from '../../../lib/handleFollow';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handler: Handler = async (event, context) => {
 | 
				
			||||||
 | 
					  if (event.httpMethod !== 'POST') {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 405,
 | 
				
			||||||
 | 
					      body: 'Method Not Allowed',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const inboxData = JSON.parse(event.body || '{}');
 | 
				
			||||||
 | 
					    const username = event.path.split('/')[2]; // /users/:username/inbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isVerified = await verifySignature(event.headers, event.body);
 | 
				
			||||||
 | 
					    if (!isVerified) {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        statusCode: 401,
 | 
				
			||||||
 | 
					        body: 'Unauthorized',
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (inboxData.type === 'Follow') {
 | 
				
			||||||
 | 
					      await handleFollow(inboxData, username);
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        statusCode: 202,
 | 
				
			||||||
 | 
					        body: 'Follow request accepted',
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 200,
 | 
				
			||||||
 | 
					      body: 'Activity received',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 500,
 | 
				
			||||||
 | 
					      body: JSON.stringify({ error: error.message }),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { handler };
 | 
				
			||||||
							
								
								
									
										20
									
								
								netlify/functions/outbox.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								netlify/functions/outbox.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { Handler } from '@netlify/functions';
 | 
				
			||||||
 | 
					import { join } from 'path';
 | 
				
			||||||
 | 
					import { cwd } from 'process';
 | 
				
			||||||
 | 
					import { readFileSync } from 'fs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handler: Handler = async (event, context) => {
 | 
				
			||||||
 | 
					  const file = join(cwd(), 'public', 'outbox.ajson');
 | 
				
			||||||
 | 
					  const stringified = readFileSync(file, 'utf8');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    statusCode: 200,
 | 
				
			||||||
 | 
					    headers: {
 | 
				
			||||||
 | 
					      "Content-Type": "application/activity+json",
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    body: stringified,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { handler };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										100
									
								
								netlify/functions/sendNote.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								netlify/functions/sendNote.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					import { Handler } from '@netlify/functions';
 | 
				
			||||||
 | 
					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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handler: Handler = async (event, context) => {
 | 
				
			||||||
 | 
					  if (event.headers.authorization !== `Bearer ${process.env.CRON_SECRET}`) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      statusCode: 401,
 | 
				
			||||||
 | 
					      body: "Unauthorized",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const configRef = db.collection('config').doc('config');
 | 
				
			||||||
 | 
					  const config = await configRef.get();
 | 
				
			||||||
 | 
					  const lastId = config.exists ? config.data()?.lastId || '' : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Fetch notes from outbox
 | 
				
			||||||
 | 
					  const outboxResponse = await fetch("https://coder.is-a.dev/api/activitypub/outbox");
 | 
				
			||||||
 | 
					  const outbox = <OrderedCollection>await outboxResponse.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const followersSnapshot = await db.collection('followers').get();
 | 
				
			||||||
 | 
					  let lastSuccessfulSentId = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const inboxes = new Set<string>(); // Track unique inboxes to avoid duplicate sending
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const followerDoc of followersSnapshot.docs) {
 | 
				
			||||||
 | 
					    const follower = followerDoc.data();
 | 
				
			||||||
 | 
					    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 || !actorInformation.inbox) {
 | 
				
			||||||
 | 
					      console.log(`Skipping ${actorUrl}: No valid inbox`);
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const inboxUrl = actorInformation.sharedInbox || actorInformation.inbox;
 | 
				
			||||||
 | 
					    inboxes.add(inboxUrl.toString()); // Add to set to ensure unique delivery
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const item of <AP.EntityReference[]>outbox.orderedItems) {
 | 
				
			||||||
 | 
					    if (item.id === lastId) {
 | 
				
			||||||
 | 
					      console.log(`${item.id} has already been posted - skipping`);
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (item.object) {
 | 
				
			||||||
 | 
					      item.object.published = new Date().toISOString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const inboxUrl of inboxes) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        console.log(`Sending to ${inboxUrl}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const response = await sendSignedRequest(new URL(inboxUrl), <AP.Activity>item, {
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            "Accept": "application/activity+json",
 | 
				
			||||||
 | 
					            "Content-Type": "application/activity+json",
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(`Send result: ${response.status} ${response.statusText}`);
 | 
				
			||||||
 | 
					        const responseText = await response.text();
 | 
				
			||||||
 | 
					        console.log("Response body:", responseText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        lastSuccessfulSentId = item.id;
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error(`Error sending to ${inboxUrl}:`, error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    break; // Only send the latest post for now
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await configRef.set({ lastId: lastSuccessfulSentId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    statusCode: 200,
 | 
				
			||||||
 | 
					    body: "ok",
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { handler };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user