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 { 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 = {
|
export async function countFollowing(actor: Actor): Promise<number> {
|
||||||
accept: 'application/activity+json',
|
const collection = await getMetadata(actor.following)
|
||||||
|
return collection.totalItems
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFollowingMetadata(actor: Actor): Promise<OrderedCollection<unknown>> {
|
export async function countFollowers(actor: Actor): Promise<number> {
|
||||||
const res = await fetch(actor.following, { headers })
|
const collection = await getMetadata(actor.followers)
|
||||||
if (!res.ok) {
|
return collection.totalItems
|
||||||
throw new Error(`${actor.following} returned ${res.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json<OrderedCollection<unknown>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFollowersMetadata(actor: Actor): Promise<OrderedCollection<unknown>> {
|
export async function getFollowers(actor: Actor): Promise<OrderedCollection<string>> {
|
||||||
const res = await fetch(actor.followers, { headers })
|
const collection = await getMetadata(actor.followers)
|
||||||
if (!res.ok) {
|
collection.items = await loadItems<string>(collection)
|
||||||
throw new Error(`${actor.followers} returned ${res.status}`)
|
return collection
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json<OrderedCollection<unknown>>()
|
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 { Object } from 'wildebeest/backend/src/activitypub/objects'
|
||||||
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
|
import type { Activity } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
import type { Actor } from 'wildebeest/backend/src/activitypub/actors'
|
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'
|
import { PUBLIC_GROUP } from 'wildebeest/backend/src/activitypub/activities'
|
||||||
|
|
||||||
export async function addObjectInOutbox(
|
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>> {
|
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)
|
collection.items = await loadItems(collection, 20)
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export async function countStatuses(actor: Actor): Promise<number> {
|
||||||
async function loadItems<T>(collection: OrderedCollection<T>, max: number): Promise<Array<T>> {
|
const metadata = await getMetadata(actor.outbox)
|
||||||
// FIXME: implement max and multi page support
|
return metadata.totalItems
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// Load an external user, using ActivityPub queries, and return it as a MastodonAccount
|
||||||
export async function loadExternalMastodonAccount(
|
export async function loadExternalMastodonAccount(
|
||||||
acct: string,
|
acct: string,
|
||||||
res: Actor,
|
actor: Actor,
|
||||||
loadStats: boolean = false
|
loadStats: boolean = false
|
||||||
): Promise<MastodonAccount> {
|
): Promise<MastodonAccount> {
|
||||||
const account = toMastodonAccount(acct, res)
|
const account = toMastodonAccount(acct, actor)
|
||||||
if (loadStats === true) {
|
if (loadStats === true) {
|
||||||
account.statuses_count = (await apOutbox.getMetadata(res)).totalItems
|
account.statuses_count = await apOutbox.countStatuses(actor)
|
||||||
account.followers_count = (await apFollow.getFollowersMetadata(res)).totalItems
|
account.followers_count = await apFollow.countFollowers(actor)
|
||||||
account.following_count = (await apFollow.getFollowingMetadata(res)).totalItems
|
account.following_count = await apFollow.countFollowing(actor)
|
||||||
}
|
}
|
||||||
return account
|
return account
|
||||||
}
|
}
|
||||||
|
|
|
@ -638,10 +638,82 @@ describe('Mastodon APIs', () => {
|
||||||
|
|
||||||
test('get remote actor followers', async () => {
|
test('get remote actor followers', async () => {
|
||||||
const db = await makeDB()
|
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 req = new Request(`https://${domain}`)
|
||||||
const res = await accounts_followers.handleRequest(req, db, 'sven@example.com', connectedActor)
|
const res = await accounts_followers.handleRequest(req, db, 'sven@example.com')
|
||||||
assert.equal(res.status, 403)
|
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 () => {
|
test('get local actor followers', async () => {
|
||||||
|
@ -664,9 +736,8 @@ describe('Mastodon APIs', () => {
|
||||||
await addFollowing(db, actor2, actor, 'sven@' + domain)
|
await addFollowing(db, actor2, actor, 'sven@' + domain)
|
||||||
await acceptFollowing(db, actor2, actor)
|
await acceptFollowing(db, actor2, actor)
|
||||||
|
|
||||||
const connectedActor = actor
|
|
||||||
const req = new Request(`https://${domain}`)
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const data = await res.json<Array<any>>()
|
const data = await res.json<Array<any>>()
|
||||||
|
@ -693,9 +764,8 @@ describe('Mastodon APIs', () => {
|
||||||
await addFollowing(db, actor, actor2, 'sven@' + domain)
|
await addFollowing(db, actor, actor2, 'sven@' + domain)
|
||||||
await acceptFollowing(db, actor, actor2)
|
await acceptFollowing(db, actor, actor2)
|
||||||
|
|
||||||
const connectedActor = actor
|
|
||||||
const req = new Request(`https://${domain}`)
|
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)
|
assert.equal(res.status, 200)
|
||||||
|
|
||||||
const data = await res.json<Array<any>>()
|
const data = await res.json<Array<any>>()
|
||||||
|
@ -704,11 +774,82 @@ describe('Mastodon APIs', () => {
|
||||||
|
|
||||||
test('get remote actor following', async () => {
|
test('get remote actor following', async () => {
|
||||||
const db = await makeDB()
|
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 req = new Request(`https://${domain}`)
|
||||||
const res = await accounts_following.handleRequest(req, db, 'sven@example.com', connectedActor)
|
const res = await accounts_following.handleRequest(req, db, 'sven@example.com')
|
||||||
assert.equal(res.status, 403)
|
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 () => {
|
test('get remote actor featured_tags', async () => {
|
||||||
|
|
|
@ -1,36 +1,70 @@
|
||||||
// https://docs.joinmastodon.org/methods/accounts/#followers
|
// 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 { cors } from 'wildebeest/backend/src/utils/cors'
|
||||||
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
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 { 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 { 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 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 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 }) => {
|
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, request, env }) => {
|
||||||
return handleRequest(request, env.DATABASE, params.id as string, data.connectedActor)
|
return handleRequest(request, env.DATABASE, params.id as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRequest(
|
export async function handleRequest(request: Request, db: D1Database, id: string): Promise<Response> {
|
||||||
request: Request,
|
|
||||||
db: D1Database,
|
|
||||||
id: string,
|
|
||||||
connectedActor: Person
|
|
||||||
): Promise<Response> {
|
|
||||||
const handle = parseHandle(id)
|
const handle = parseHandle(id)
|
||||||
const domain = new URL(request.url).hostname
|
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 out: Array<MastodonAccount> = []
|
||||||
|
|
||||||
const followers = await getFollowers(db, connectedActor)
|
|
||||||
for (let i = 0, len = followers.length; i < len; i++) {
|
for (let i = 0, len = followers.length; i < len; i++) {
|
||||||
const id = new URL(followers[i])
|
const id = new URL(followers[i])
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,70 @@
|
||||||
// https://docs.joinmastodon.org/methods/accounts/#following
|
// 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 { cors } from 'wildebeest/backend/src/utils/cors'
|
||||||
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
import { loadExternalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
||||||
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
||||||
import { urlToHandle } from 'wildebeest/backend/src/utils/handle'
|
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 { 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 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 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 }) => {
|
export const onRequest: PagesFunction<Env, any, ContextData> = async ({ params, request, env }) => {
|
||||||
return handleRequest(request, env.DATABASE, params.id as string, data.connectedActor)
|
return handleRequest(request, env.DATABASE, params.id as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRequest(
|
export async function handleRequest(request: Request, db: D1Database, id: string): Promise<Response> {
|
||||||
request: Request,
|
|
||||||
db: D1Database,
|
|
||||||
id: string,
|
|
||||||
connectedActor: Person
|
|
||||||
): Promise<Response> {
|
|
||||||
const handle = parseHandle(id)
|
const handle = parseHandle(id)
|
||||||
const domain = new URL(request.url).hostname
|
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 out: Array<MastodonAccount> = []
|
||||||
|
|
||||||
const following = await getFollowingId(db, connectedActor)
|
|
||||||
for (let i = 0, len = following.length; i < len; i++) {
|
for (let i = 0, len = following.length; i < len; i++) {
|
||||||
const id = new URL(following[i])
|
const id = new URL(following[i])
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue