kopia lustrzana https://github.com/cloudflare/wildebeest
				
				
				
			MOW-87: implement remote followers/following
							rodzic
							
								
									a9d2b676fb
								
							
						
					
					
						commit
						4a0d413c00
					
				| 
						 | 
				
			
			@ -1,24 +1,35 @@
 | 
			
		|||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import type { OrderedCollection } from 'wildebeest/backend/src/activitypub/core'
 | 
			
		||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import type { OrderedCollection } from 'wildebeest/backend/src/activitypub/objects/collection'
 | 
			
		||||
import { getMetadata, loadItems } from 'wildebeest/backend/src/activitypub/objects/collection'
 | 
			
		||||
 | 
			
		||||
const headers = {
 | 
			
		||||
	accept: 'application/activity+json',
 | 
			
		||||
export async function countFollowing(actor: Actor): Promise<number> {
 | 
			
		||||
	const collection = await getMetadata(actor.following)
 | 
			
		||||
	return collection.totalItems
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getFollowingMetadata(actor: Actor): Promise<OrderedCollection<unknown>> {
 | 
			
		||||
	const res = await fetch(actor.following, { headers })
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		throw new Error(`${actor.following} returned ${res.status}`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.json<OrderedCollection<unknown>>()
 | 
			
		||||
export async function countFollowers(actor: Actor): Promise<number> {
 | 
			
		||||
	const collection = await getMetadata(actor.followers)
 | 
			
		||||
	return collection.totalItems
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getFollowersMetadata(actor: Actor): Promise<OrderedCollection<unknown>> {
 | 
			
		||||
	const res = await fetch(actor.followers, { headers })
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		throw new Error(`${actor.followers} returned ${res.status}`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.json<OrderedCollection<unknown>>()
 | 
			
		||||
export async function getFollowers(actor: Actor): Promise<OrderedCollection<string>> {
 | 
			
		||||
	const collection = await getMetadata(actor.followers)
 | 
			
		||||
	collection.items = await loadItems<string>(collection)
 | 
			
		||||
	return collection
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getFollowing(actor: Actor): Promise<OrderedCollection<string>> {
 | 
			
		||||
	const collection = await getMetadata(actor.following)
 | 
			
		||||
	collection.items = await loadItems<string>(collection)
 | 
			
		||||
	return collection
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function loadActors(db: D1Database, collection: OrderedCollection<string>): Promise<Array<Actor>> {
 | 
			
		||||
	const promises = collection.items.map((item) => {
 | 
			
		||||
		const actorId = new URL(item)
 | 
			
		||||
		return actors.getAndCache(actorId, db)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return Promise.all(promises)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
import type { Object } from 'wildebeest/backend/src/activitypub/objects'
 | 
			
		||||
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
 | 
			
		||||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import type { OrderedCollection, OrderedCollectionPage } from 'wildebeest/backend/src/activitypub/core'
 | 
			
		||||
import type { OrderedCollection } from 'wildebeest/backend/src/activitypub/objects/collection'
 | 
			
		||||
import { getMetadata, loadItems } from 'wildebeest/backend/src/activitypub/objects/collection'
 | 
			
		||||
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
 | 
			
		||||
 | 
			
		||||
export async function addObjectInOutbox(
 | 
			
		||||
| 
						 | 
				
			
			@ -30,35 +31,14 @@ export async function addObjectInOutbox(
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headers = {
 | 
			
		||||
	accept: 'application/activity+json',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getMetadata(actor: Actor): Promise<OrderedCollection<unknown>> {
 | 
			
		||||
	const res = await fetch(actor.outbox, { headers })
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		throw new Error(`${actor.outbox} returned ${res.status}`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.json<OrderedCollection<unknown>>()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function get(actor: Actor): Promise<OrderedCollection<Activity>> {
 | 
			
		||||
	const collection = await getMetadata(actor)
 | 
			
		||||
	const collection = await getMetadata(actor.outbox)
 | 
			
		||||
	collection.items = await loadItems(collection, 20)
 | 
			
		||||
 | 
			
		||||
	return collection
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
async function loadItems<T>(collection: OrderedCollection<T>, max: number): Promise<Array<T>> {
 | 
			
		||||
	// FIXME: implement max and multi page support
 | 
			
		||||
 | 
			
		||||
	const res = await fetch(collection.first, { headers })
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		throw new Error(`${collection.first} returned ${res.status}`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const data = await res.json<OrderedCollectionPage<T>>()
 | 
			
		||||
	return data.orderedItems
 | 
			
		||||
export async function countStatuses(actor: Actor): Promise<number> {
 | 
			
		||||
	const metadata = await getMetadata(actor.outbox)
 | 
			
		||||
	return metadata.totalItems
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
import type { Object } from 'wildebeest/backend/src/activitypub/objects'
 | 
			
		||||
 | 
			
		||||
export interface Collection<T> extends Object {
 | 
			
		||||
	totalItems: number
 | 
			
		||||
	current?: string
 | 
			
		||||
	first: URL
 | 
			
		||||
	last: URL
 | 
			
		||||
	items: Array<T>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OrderedCollection<T> extends Collection<T> {}
 | 
			
		||||
 | 
			
		||||
export interface OrderedCollectionPage<T> extends Object {
 | 
			
		||||
	orderedItems: Array<T>
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
import type { Object } from 'wildebeest/backend/src/activitypub/objects'
 | 
			
		||||
 | 
			
		||||
export interface Collection<T> extends Object {
 | 
			
		||||
	totalItems: number
 | 
			
		||||
	current?: string
 | 
			
		||||
	first: URL
 | 
			
		||||
	last: URL
 | 
			
		||||
	items: Array<T>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OrderedCollection<T> extends Collection<T> {}
 | 
			
		||||
 | 
			
		||||
export interface OrderedCollectionPage<T> extends Object {
 | 
			
		||||
	orderedItems: Array<T>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const headers = {
 | 
			
		||||
	accept: 'application/activity+json',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getMetadata(url: URL): Promise<OrderedCollection<any>> {
 | 
			
		||||
	const res = await fetch(url, { headers })
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		throw new Error(`${url} returned ${res.status}`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return res.json<OrderedCollection<any>>()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
export async function loadItems<T>(collection: OrderedCollection<T>, max?: number): Promise<Array<T>> {
 | 
			
		||||
	// FIXME: implement max and multi page support
 | 
			
		||||
 | 
			
		||||
	const res = await fetch(collection.first, { headers })
 | 
			
		||||
	if (!res.ok) {
 | 
			
		||||
		throw new Error(`${collection.first} returned ${res.status}`)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const data = await res.json<OrderedCollectionPage<T>>()
 | 
			
		||||
	return data.orderedItems
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -42,14 +42,14 @@ function toMastodonAccount(acct: string, res: Actor): MastodonAccount {
 | 
			
		|||
// Load an external user, using ActivityPub queries, and return it as a MastodonAccount
 | 
			
		||||
export async function loadExternalMastodonAccount(
 | 
			
		||||
	acct: string,
 | 
			
		||||
	res: Actor,
 | 
			
		||||
	actor: Actor,
 | 
			
		||||
	loadStats: boolean = false
 | 
			
		||||
): Promise<MastodonAccount> {
 | 
			
		||||
	const account = toMastodonAccount(acct, res)
 | 
			
		||||
	const account = toMastodonAccount(acct, actor)
 | 
			
		||||
	if (loadStats === true) {
 | 
			
		||||
		account.statuses_count = (await apOutbox.getMetadata(res)).totalItems
 | 
			
		||||
		account.followers_count = (await apFollow.getFollowersMetadata(res)).totalItems
 | 
			
		||||
		account.following_count = (await apFollow.getFollowingMetadata(res)).totalItems
 | 
			
		||||
		account.statuses_count = await apOutbox.countStatuses(actor)
 | 
			
		||||
		account.followers_count = await apFollow.countFollowers(actor)
 | 
			
		||||
		account.following_count = await apFollow.countFollowing(actor)
 | 
			
		||||
	}
 | 
			
		||||
	return account
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -638,10 +638,82 @@ describe('Mastodon APIs', () => {
 | 
			
		|||
 | 
			
		||||
		test('get remote actor followers', async () => {
 | 
			
		||||
			const db = await makeDB()
 | 
			
		||||
			const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
 | 
			
		||||
			const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
 | 
			
		||||
 | 
			
		||||
			globalThis.fetch = async (input: any) => {
 | 
			
		||||
				if (input.toString() === 'https://example.com/.well-known/webfinger?resource=acct%3Asven%40example.com') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							links: [
 | 
			
		||||
								{
 | 
			
		||||
									rel: 'self',
 | 
			
		||||
									type: 'application/activity+json',
 | 
			
		||||
									href: 'https://example.com/users/sven',
 | 
			
		||||
								},
 | 
			
		||||
							],
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/sven') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							id: 'https://example.com/users/sven',
 | 
			
		||||
							type: 'Person',
 | 
			
		||||
							followers: 'https://example.com/users/sven/followers',
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/sven/followers') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							'@context': 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
							id: 'https://example.com/users/sven/followers',
 | 
			
		||||
							type: 'OrderedCollection',
 | 
			
		||||
							totalItems: 3,
 | 
			
		||||
							first: 'https://example.com/users/sven/followers/1',
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/sven/followers/1') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							'@context': 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
							id: 'https://example.com/users/sven/followers/1',
 | 
			
		||||
							type: 'OrderedCollectionPage',
 | 
			
		||||
							totalItems: 3,
 | 
			
		||||
							partOf: 'https://example.com/users/sven/followers',
 | 
			
		||||
							orderedItems: [
 | 
			
		||||
								actorA.id.toString(), // local user
 | 
			
		||||
								'https://example.com/users/b', // remote user
 | 
			
		||||
							],
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/b') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							id: 'https://example.com/users/b',
 | 
			
		||||
							type: 'Person',
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				throw new Error('unexpected request to ' + input)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const req = new Request(`https://${domain}`)
 | 
			
		||||
			const res = await accounts_followers.handleRequest(req, db, 'sven@example.com', connectedActor)
 | 
			
		||||
			assert.equal(res.status, 403)
 | 
			
		||||
			const res = await accounts_followers.handleRequest(req, db, 'sven@example.com')
 | 
			
		||||
			assert.equal(res.status, 200)
 | 
			
		||||
 | 
			
		||||
			const data = await res.json<Array<any>>()
 | 
			
		||||
			assert.equal(data.length, 2)
 | 
			
		||||
 | 
			
		||||
			assert.equal(data[0].acct, 'a@cloudflare.com')
 | 
			
		||||
			assert.equal(data[1].acct, 'b@example.com')
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		test('get local actor followers', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -664,9 +736,8 @@ describe('Mastodon APIs', () => {
 | 
			
		|||
			await addFollowing(db, actor2, actor, 'sven@' + domain)
 | 
			
		||||
			await acceptFollowing(db, actor2, actor)
 | 
			
		||||
 | 
			
		||||
			const connectedActor = actor
 | 
			
		||||
			const req = new Request(`https://${domain}`)
 | 
			
		||||
			const res = await accounts_followers.handleRequest(req, db, 'sven', connectedActor)
 | 
			
		||||
			const res = await accounts_followers.handleRequest(req, db, 'sven')
 | 
			
		||||
			assert.equal(res.status, 200)
 | 
			
		||||
 | 
			
		||||
			const data = await res.json<Array<any>>()
 | 
			
		||||
| 
						 | 
				
			
			@ -693,9 +764,8 @@ describe('Mastodon APIs', () => {
 | 
			
		|||
			await addFollowing(db, actor, actor2, 'sven@' + domain)
 | 
			
		||||
			await acceptFollowing(db, actor, actor2)
 | 
			
		||||
 | 
			
		||||
			const connectedActor = actor
 | 
			
		||||
			const req = new Request(`https://${domain}`)
 | 
			
		||||
			const res = await accounts_following.handleRequest(req, db, 'sven', connectedActor)
 | 
			
		||||
			const res = await accounts_following.handleRequest(req, db, 'sven')
 | 
			
		||||
			assert.equal(res.status, 200)
 | 
			
		||||
 | 
			
		||||
			const data = await res.json<Array<any>>()
 | 
			
		||||
| 
						 | 
				
			
			@ -704,11 +774,82 @@ describe('Mastodon APIs', () => {
 | 
			
		|||
 | 
			
		||||
		test('get remote actor following', async () => {
 | 
			
		||||
			const db = await makeDB()
 | 
			
		||||
			const actorA = await createPerson(domain, db, userKEK, 'a@cloudflare.com')
 | 
			
		||||
 | 
			
		||||
			globalThis.fetch = async (input: any) => {
 | 
			
		||||
				if (input.toString() === 'https://example.com/.well-known/webfinger?resource=acct%3Asven%40example.com') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							links: [
 | 
			
		||||
								{
 | 
			
		||||
									rel: 'self',
 | 
			
		||||
									type: 'application/activity+json',
 | 
			
		||||
									href: 'https://example.com/users/sven',
 | 
			
		||||
								},
 | 
			
		||||
							],
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/sven') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							id: 'https://example.com/users/sven',
 | 
			
		||||
							type: 'Person',
 | 
			
		||||
							following: 'https://example.com/users/sven/following',
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/sven/following') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							'@context': 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
							id: 'https://example.com/users/sven/following',
 | 
			
		||||
							type: 'OrderedCollection',
 | 
			
		||||
							totalItems: 3,
 | 
			
		||||
							first: 'https://example.com/users/sven/following/1',
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/sven/following/1') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							'@context': 'https://www.w3.org/ns/activitystreams',
 | 
			
		||||
							id: 'https://example.com/users/sven/following/1',
 | 
			
		||||
							type: 'OrderedCollectionPage',
 | 
			
		||||
							totalItems: 3,
 | 
			
		||||
							partOf: 'https://example.com/users/sven/following',
 | 
			
		||||
							orderedItems: [
 | 
			
		||||
								actorA.id.toString(), // local user
 | 
			
		||||
								'https://example.com/users/b', // remote user
 | 
			
		||||
							],
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (input.toString() === 'https://example.com/users/b') {
 | 
			
		||||
					return new Response(
 | 
			
		||||
						JSON.stringify({
 | 
			
		||||
							id: 'https://example.com/users/b',
 | 
			
		||||
							type: 'Person',
 | 
			
		||||
						})
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				throw new Error('unexpected request to ' + input)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const connectedActor = await createPerson(domain, db, userKEK, 'sven@cloudflare.com')
 | 
			
		||||
			const req = new Request(`https://${domain}`)
 | 
			
		||||
			const res = await accounts_following.handleRequest(req, db, 'sven@example.com', connectedActor)
 | 
			
		||||
			assert.equal(res.status, 403)
 | 
			
		||||
			const res = await accounts_following.handleRequest(req, db, 'sven@example.com')
 | 
			
		||||
			assert.equal(res.status, 200)
 | 
			
		||||
 | 
			
		||||
			const data = await res.json<Array<any>>()
 | 
			
		||||
			assert.equal(data.length, 2)
 | 
			
		||||
 | 
			
		||||
			assert.equal(data[0].acct, 'a@cloudflare.com')
 | 
			
		||||
			assert.equal(data[1].acct, 'b@example.com')
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		test('get remote actor featured_tags', async () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +1,70 @@
 | 
			
		|||
// https://docs.joinmastodon.org/methods/accounts/#followers
 | 
			
		||||
 | 
			
		||||
import type { Handle } from 'wildebeest/backend/src/utils/parse'
 | 
			
		||||
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import { cors } from 'wildebeest/backend/src/utils/cors'
 | 
			
		||||
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
 | 
			
		||||
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
 | 
			
		||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
 | 
			
		||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
 | 
			
		||||
import { MastodonAccount } from 'wildebeest/backend/src/types/account'
 | 
			
		||||
import type { Person } from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
 | 
			
		||||
import { getFollowers } from 'wildebeest/backend/src/mastodon/follow'
 | 
			
		||||
import type { Env } from 'wildebeest/backend/src/types/env'
 | 
			
		||||
import { domainNotAuthorized } from 'wildebeest/backend/src/errors'
 | 
			
		||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import * as webfinger from 'wildebeest/backend/src/webfinger'
 | 
			
		||||
import { getFollowers, loadActors } from 'wildebeest/backend/src/activitypub/actors/follow'
 | 
			
		||||
import * as localFollow from 'wildebeest/backend/src/mastodon/follow'
 | 
			
		||||
 | 
			
		||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, request, env, data }) => {
 | 
			
		||||
	return handleRequest(request, env.DATABASE, params.id as string, data.connectedActor)
 | 
			
		||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, request, env }) => {
 | 
			
		||||
	return handleRequest(request, env.DATABASE, params.id as string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleRequest(
 | 
			
		||||
	request: Request,
 | 
			
		||||
	db: D1Database,
 | 
			
		||||
	id: string,
 | 
			
		||||
	connectedActor: Person
 | 
			
		||||
): Promise<Response> {
 | 
			
		||||
export async function handleRequest(request: Request, db: D1Database, id: string): Promise<Response> {
 | 
			
		||||
	const handle = parseHandle(id)
 | 
			
		||||
	const domain = new URL(request.url).hostname
 | 
			
		||||
	if (handle.domain !== null && handle.domain !== domain) {
 | 
			
		||||
		return domainNotAuthorized()
 | 
			
		||||
 | 
			
		||||
	if (handle.domain === null || (handle.domain !== null && handle.domain === domain)) {
 | 
			
		||||
		// Retrieve the infos from a local user
 | 
			
		||||
		return getLocalFollowers(request, handle, db)
 | 
			
		||||
	} else if (handle.domain !== null) {
 | 
			
		||||
		// Retrieve the infos of a remote actor
 | 
			
		||||
		return getRemoteFollowers(request, handle, db)
 | 
			
		||||
	} else {
 | 
			
		||||
		return new Response('', { status: 403 })
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getRemoteFollowers(request: Request, handle: Handle, db: D1Database): Promise<Response> {
 | 
			
		||||
	const acct = `${handle.localPart}@${handle.domain}`
 | 
			
		||||
	const link = await webfinger.queryAcctLink(handle.domain!, acct)
 | 
			
		||||
	if (link === null) {
 | 
			
		||||
		return new Response('', { status: 404 })
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const actor = await actors.getAndCache(link, db)
 | 
			
		||||
	const followersIds = await getFollowers(actor)
 | 
			
		||||
	const followers = await loadActors(db, followersIds)
 | 
			
		||||
 | 
			
		||||
	const promises = followers.map((actor) => {
 | 
			
		||||
		const acct = urlToHandle(actor.id)
 | 
			
		||||
		return loadExternalMastodonAccount(acct, actor, false)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	const out = await Promise.all(promises)
 | 
			
		||||
	const headers = {
 | 
			
		||||
		...cors(),
 | 
			
		||||
		'content-type': 'application/json; charset=utf-8',
 | 
			
		||||
	}
 | 
			
		||||
	return new Response(JSON.stringify(out), { headers })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getLocalFollowers(request: Request, handle: Handle, db: D1Database): Promise<Response> {
 | 
			
		||||
	const domain = new URL(request.url).hostname
 | 
			
		||||
	const actorId = actorURL(domain, handle.localPart)
 | 
			
		||||
	const actor = await actors.getAndCache(actorId, db)
 | 
			
		||||
 | 
			
		||||
	const followers = await localFollow.getFollowers(db, actor)
 | 
			
		||||
	const out: Array<MastodonAccount> = []
 | 
			
		||||
 | 
			
		||||
	const followers = await getFollowers(db, connectedActor)
 | 
			
		||||
	for (let i = 0, len = followers.length; i < len; i++) {
 | 
			
		||||
		const id = new URL(followers[i])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +1,70 @@
 | 
			
		|||
// https://docs.joinmastodon.org/methods/accounts/#following
 | 
			
		||||
 | 
			
		||||
import type { Handle } from 'wildebeest/backend/src/utils/parse'
 | 
			
		||||
import { actorURL } from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import { cors } from 'wildebeest/backend/src/utils/cors'
 | 
			
		||||
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
 | 
			
		||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
 | 
			
		||||
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
 | 
			
		||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import { MastodonAccount } from 'wildebeest/backend/src/types/account'
 | 
			
		||||
import type { Person } from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import type { ContextData } from 'wildebeest/backend/src/types/context'
 | 
			
		||||
import { getFollowingId } from 'wildebeest/backend/src/mastodon/follow'
 | 
			
		||||
import * as localFollow from 'wildebeest/backend/src/mastodon/follow'
 | 
			
		||||
import type { Env } from 'wildebeest/backend/src/types/env'
 | 
			
		||||
import { domainNotAuthorized } from 'wildebeest/backend/src/errors'
 | 
			
		||||
import * as actors from 'wildebeest/backend/src/activitypub/actors'
 | 
			
		||||
import * as webfinger from 'wildebeest/backend/src/webfinger'
 | 
			
		||||
import { getFollowing, loadActors } from 'wildebeest/backend/src/activitypub/actors/follow'
 | 
			
		||||
 | 
			
		||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, request, env, data }) => {
 | 
			
		||||
	return handleRequest(request, env.DATABASE, params.id as string, data.connectedActor)
 | 
			
		||||
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, request, env }) => {
 | 
			
		||||
	return handleRequest(request, env.DATABASE, params.id as string)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function handleRequest(
 | 
			
		||||
	request: Request,
 | 
			
		||||
	db: D1Database,
 | 
			
		||||
	id: string,
 | 
			
		||||
	connectedActor: Person
 | 
			
		||||
): Promise<Response> {
 | 
			
		||||
export async function handleRequest(request: Request, db: D1Database, id: string): Promise<Response> {
 | 
			
		||||
	const handle = parseHandle(id)
 | 
			
		||||
	const domain = new URL(request.url).hostname
 | 
			
		||||
	if (handle.domain !== null && handle.domain !== domain) {
 | 
			
		||||
		return domainNotAuthorized()
 | 
			
		||||
 | 
			
		||||
	if (handle.domain === null || (handle.domain !== null && handle.domain === domain)) {
 | 
			
		||||
		// Retrieve the infos from a local user
 | 
			
		||||
		return getLocalFollowing(request, handle, db)
 | 
			
		||||
	} else if (handle.domain !== null) {
 | 
			
		||||
		// Retrieve the infos of a remote actor
 | 
			
		||||
		return getRemoteFollowing(request, handle, db)
 | 
			
		||||
	} else {
 | 
			
		||||
		return new Response('', { status: 403 })
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getRemoteFollowing(request: Request, handle: Handle, db: D1Database): Promise<Response> {
 | 
			
		||||
	const acct = `${handle.localPart}@${handle.domain}`
 | 
			
		||||
	const link = await webfinger.queryAcctLink(handle.domain!, acct)
 | 
			
		||||
	if (link === null) {
 | 
			
		||||
		return new Response('', { status: 404 })
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const actor = await actors.getAndCache(link, db)
 | 
			
		||||
	const followingIds = await getFollowing(actor)
 | 
			
		||||
	const following = await loadActors(db, followingIds)
 | 
			
		||||
 | 
			
		||||
	const promises = following.map((actor) => {
 | 
			
		||||
		const acct = urlToHandle(actor.id)
 | 
			
		||||
		return loadExternalMastodonAccount(acct, actor, false)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	const out = await Promise.all(promises)
 | 
			
		||||
	const headers = {
 | 
			
		||||
		...cors(),
 | 
			
		||||
		'content-type': 'application/json; charset=utf-8',
 | 
			
		||||
	}
 | 
			
		||||
	return new Response(JSON.stringify(out), { headers })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getLocalFollowing(request: Request, handle: Handle, db: D1Database): Promise<Response> {
 | 
			
		||||
	const domain = new URL(request.url).hostname
 | 
			
		||||
	const actorId = actorURL(domain, handle.localPart)
 | 
			
		||||
	const actor = await actors.getAndCache(actorId, db)
 | 
			
		||||
 | 
			
		||||
	const following = await localFollow.getFollowingId(db, actor)
 | 
			
		||||
	const out: Array<MastodonAccount> = []
 | 
			
		||||
 | 
			
		||||
	const following = await getFollowingId(db, connectedActor)
 | 
			
		||||
	for (let i = 0, len = following.length; i < len; i++) {
 | 
			
		||||
		const id = new URL(following[i])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue