Protocole ActivityPub de Réseau Social Décentralisé

Exemples du protocole ActivityPub pour les réseaux sociaux fédérés, compatibilité Mastodon et médias sociaux décentralisés

Key Facts

Category
Web Standards
Items
3
Format Families
sample

Sample Overview

Exemples du protocole ActivityPub pour les réseaux sociaux fédérés, compatibilité Mastodon et médias sociaux décentralisés This sample set belongs to Web Standards and can be used to test related workflows inside Elysia Tools.

💻 ActivityPub Hello World typescript

🟢 simple ⭐⭐⭐

Implémentation ActivityPub de base avec acteurs, objets, activités et fondamentaux de fédération

⏱️ 30 min 🏷️ activitypub, federated, social, mastodon
Prerequisites: JavaScript/TypeScript, HTTP protocols, JSON-LD, Web standards
// ActivityPub Hello World - Basic Federated Social Network
// Simple implementation of ActivityPub protocol concepts

interface ActivityPubActor {
  '@context': ['https://www.w3.org/ns/activitystreams']
  id: string
  type: 'Person'
  name: string
  preferredUsername: string
  summary?: string
  inbox: string
  outbox: string
  followers: string
  following: string
  liked: string
  publicKey: {
    id: string
    owner: string
    publicKeyPem: string
  }
  icon?: {
    type: 'Image'
    url: string
  }
  image?: {
    type: 'Image'
    url: string
  }
}

interface ActivityPubObject {
  '@context': ['https://www.w3.org/ns/activitystreams']
  id: string
  type: 'Note' | 'Article' | 'Image' | 'Video'
  attributedTo: string
  content: string
  published: string
  to: string[]
  cc: string[]
  url?: string
  attachment?: any[]
  tag?: any[]
}

interface Activity {
  '@context': ['https://www.w3.org/ns/activitystreams']
  id: string
  type: 'Create' | 'Update' | 'Delete' | 'Follow' | 'Like' | 'Announce' | 'Accept' | 'Undo'
  actor: string
  object: ActivityPubObject | string | Activity
  target?: string
  to: string[]
  cc: string[]
  published: string
}

class ActivityPubHelloWorld {
  private actors: Map<string, ActivityPubActor> = new Map()
  private activities: Map<string, Activity> = new Map()
  private objects: Map<string, ActivityPubObject> = new Map()
  private followers: Map<string, Set<string>> = new Map()
  private following: Map<string, Set<string>> = new Map()

  constructor(private domain: string) {
    console.log(`ActivityPub server for ${domain} initialized`)
  }

  // Generate unique ID
  private generateId(type: string): string {
    return `https://${this.domain}/${type}/${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  }

  // Generate key pair for actor
  private async generateKeyPair(): Promise<{ publicKeyPem: string; privateKeyPem: string }> {
    // In a real implementation, use a proper crypto library
    // This is a simplified example
    return {
      publicKeyPem: `-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA${this.generateId('key')}\\n-----END PUBLIC KEY-----`,
      privateKeyPem: `-----BEGIN PRIVATE KEY-----\\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC${this.generateId('key')}\\n-----END PRIVATE KEY-----`
    }
  }

  // Create a new actor (user)
  async createActor(username: string, name: string, summary?: string): Promise<ActivityPubActor> {
    const actorId = `https://${this.domain}/users/${username}`

    // Check if actor already exists
    if (this.actors.has(actorId)) {
      throw new Error('Actor already exists')
    }

    // Generate key pair
    const { publicKeyPem } = await this.generateKeyPair()

    const actor: ActivityPubActor = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: actorId,
      type: 'Person',
      name,
      preferredUsername: username,
      summary,
      inbox: `${actorId}/inbox`,
      outbox: `${actorId}/outbox`,
      followers: `${actorId}/followers`,
      following: `${actorId}/following`,
      liked: `${actorId}/liked`,
      publicKey: {
        id: `${actorId}#main-key`,
        owner: actorId,
        publicKeyPem
      },
      icon: {
        type: 'Image',
        url: `https://${this.domain}/avatars/${username}.png`
      }
    }

    // Store actor
    this.actors.set(actorId, actor)
    this.followers.set(actorId, new Set())
    this.following.set(actorId, new Set())

    console.log(`Created actor: ${actorId}`)
    return actor
  }

  // Create and publish a note
  async createNote(actorId: string, content: string, to: string[] = ['https://www.w3.org/ns/activitystreams#Public']): Promise<Activity> {
    const actor = this.actors.get(actorId)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Create the note object
    const noteId = this.generateId('objects/note')
    const note: ActivityPubObject = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: noteId,
      type: 'Note',
      attributedTo: actorId,
      content,
      published: new Date().toISOString(),
      to,
      cc: [actor.followers]
    }

    // Create the Create activity
    const activityId = this.generateId('activities/create')
    const activity: Activity = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: activityId,
      type: 'Create',
      actor: actorId,
      object: note,
      to,
      cc: [actor.followers],
      published: new Date().toISOString()
    }

    // Store objects and activity
    this.objects.set(noteId, note)
    this.activities.set(activityId, activity)

    // Deliver to followers
    await this.deliverToFollowers(activityId, actorId)

    console.log(`Created note: ${noteId}`)
    return activity
  }

  // Follow another actor
  async followActor(actorId: string, targetActorId: string): Promise<Activity> {
    const actor = this.actors.get(actorId)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Create Follow activity
    const activityId = this.generateId('activities/follow')
    const activity: Activity = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: activityId,
      type: 'Follow',
      actor: actorId,
      object: targetActorId,
      to: [targetActorId],
      cc: [],
      published: new Date().toISOString()
    }

    // Store activity
    this.activities.set(activityId, activity)

    // Update following relationship
    const followingSet = this.following.get(actorId)
    if (followingSet) {
      followingSet.add(targetActorId)
    }

    // Deliver to target actor
    await this.deliverToActor(activityId, targetActorId)

    console.log(`${actorId} followed ${targetActorId}`)
    return activity
  }

  // Like an object
  async likeObject(actorId: string, objectId: string): Promise<Activity> {
    const actor = this.actors.get(actorId)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Create Like activity
    const activityId = this.generateId('activities/like')
    const activity: Activity = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: activityId,
      type: 'Like',
      actor: actorId,
      object: objectId,
      to: [objectId],
      cc: [],
      published: new Date().toISOString()
    }

    // Store activity
    this.activities.set(activityId, activity)

    // Deliver to object owner
    const object = this.objects.get(objectId)
    if (object) {
      await this.deliverToActor(activityId, object.attributedTo)
    }

    console.log(`${actorId} liked ${objectId}`)
    return activity
  }

  // Announce (boost/retweet) an object
  async announceObject(actorId: string, objectId: string): Promise<Activity> {
    const actor = this.actors.get(actorId)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Create Announce activity
    const activityId = this.generateId('activities/announce')
    const activity: Activity = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: activityId,
      type: 'Announce',
      actor: actorId,
      object: objectId,
      to: [actor.followers, 'https://www.w3.org/ns/activitystreams#Public'],
      cc: [],
      published: new Date().toISOString()
    }

    // Store activity
    this.activities.set(activityId, activity)

    // Deliver to followers
    await this.deliverToFollowers(activityId, actorId)

    console.log(`${actorId} announced ${objectId}`)
    return activity
  }

  // Get actor's outbox
  async getOutbox(actorId: string): Promise<Activity[]> {
    const actor = this.actors.get(actorId)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Get all activities by this actor
    const actorActivities = Array.from(this.activities.values())
      .filter(activity => activity.actor === actorId)
      .sort((a, b) => new Date(b.published).getTime() - new Date(a.published).getTime())

    return actorActivities
  }

  // Get actor's inbox
  async getInbox(actorId: string): Promise<Activity[]> {
    const actor = this.actors.get(actorId)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Get activities addressed to this actor
    const addressedActivities = Array.from(this.activities.values())
      .filter(activity =>
        activity.to.includes(actorId) ||
        activity.cc.includes(actorId) ||
        activity.object === actorId
      )
      .sort((a, b) => new Date(b.published).getTime() - new Date(a.published).getTime())

    return addressedActivities
  }

  // Accept a follow request
  async acceptFollow(actorId: string, followActivityId: string): Promise<Activity> {
    const followActivity = this.activities.get(followActivityId)
    if (!followActivity || followActivity.type !== 'Follow') {
      throw new Error('Invalid follow activity')
    }

    const followerId = followActivity.actor as string

    // Create Accept activity
    const activityId = this.generateId('activities/accept')
    const activity: Activity = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: activityId,
      type: 'Accept',
      actor: actorId,
      object: followActivity,
      to: [followerId],
      cc: [],
      published: new Date().toISOString()
    }

    // Store activity
    this.activities.set(activityId, activity)

    // Update followers relationship
    const followersSet = this.followers.get(actorId)
    if (followersSet) {
      followersSet.add(followerId)
    }

    // Deliver acceptance to follower
    await this.deliverToActor(activityId, followerId)

    console.log(`${actorId} accepted follow from ${followerId}`)
    return activity
  }

  // Undo an activity
  async undoActivity(actorId: string, activityId: string): Promise<Activity> {
    const originalActivity = this.activities.get(activityId)
    if (!originalActivity) {
      throw new Error('Activity not found')
    }

    // Create Undo activity
    const undoActivityId = this.generateId('activities/undo')
    const activity: Activity = {
      '@context': ['https://www.w3.org/ns/activitystreams'],
      id: undoActivityId,
      type: 'Undo',
      actor: actorId,
      object: originalActivity,
      to: originalActivity.to,
      cc: originalActivity.cc,
      published: new Date().toISOString()
    }

    // Store activity
    this.activities.set(undoActivityId, activity)

    // Handle specific undo types
    if (originalActivity.type === 'Follow') {
      const targetId = originalActivity.object as string
      const followingSet = this.following.get(actorId)
      if (followingSet) {
        followingSet.delete(targetId)
      }
    }

    // Deliver to original recipients
    await this.deliverToFollowers(undoActivityId, actorId)

    console.log(`${actorId} undone activity: ${activityId}`)
    return activity
  }

  // Server endpoints (simplified HTTP server example)
  handleWebfinger(resource: string): any {
    // Handle WebFinger discovery
    if (resource.startsWith('acct:')) {
      const [, address] = resource.split(':')
      const [username, domain] = address.split('@')

      if (domain === this.domain) {
        const actorId = `https://${this.domain}/users/${username}`
        return {
          subject: `acct:${username}@${domain}`,
          links: [{
            rel: 'self',
            type: 'application/activity+json',
            href: actorId
          }]
        }
      }
    }

    return null
  }

  handleActorRequest(actorId: string): ActivityPubActor | null {
    return this.actors.get(actorId) || null
  }

  handleInboxRequest(actorId: string, activity: Activity): Promise<void> {
    // Store incoming activity
    this.activities.set(activity.id, activity)

    // Handle activity types
    switch (activity.type) {
      case 'Follow':
        // Automatically accept follows for demo
        return this.acceptFollow(actorId, activity.id)

      case 'Like':
      case 'Announce':
        // Store likes and announces
        console.log(`Received ${activity.type} from ${activity.actor}`)
        break

      case 'Create':
        // Handle new content creation
        if (typeof activity.object === 'object') {
          const object = activity.object as ActivityPubObject
          this.objects.set(object.id, object)
        }
        break

      default:
        console.log(`Received ${activity.type} activity`)
    }

    return Promise.resolve()
  }

  // Federation delivery (simplified)
  private async deliverToFollowers(activityId: string, actorId: string): Promise<void> {
    const followersSet = this.followers.get(actorId)
    if (!followersSet) return

    const activity = this.activities.get(activityId)
    if (!activity) return

    // Deliver to each follower
    for (const followerId of followersSet) {
      await this.deliverToActor(activityId, followerId)
    }
  }

  private async deliverToActor(activityId: string, targetActorId: string): Promise<void> {
    // In a real implementation, this would:
    // 1. Sign the activity with the sender's private key
    // 2. Discover the target actor's inbox
    // 3. Send HTTP POST request to the inbox
    // 4. Handle retries and delivery failures

    console.log(`Delivering activity ${activityId} to actor ${targetActorId}`)

    // For demo purposes, we'll just log it
    const activity = this.activities.get(activityId)
    if (activity) {
      console.log('Activity payload:', JSON.stringify(activity, null, 2))
    }
  }

  // Get statistics
  getStatistics() {
    return {
      actors: this.actors.size,
      activities: this.activities.size,
      objects: this.objects.size,
      totalFollows: Array.from(this.following.values()).reduce((sum, set) => sum + set.size, 0),
      totalFollowers: Array.from(this.followers.values()).reduce((sum, set) => sum + set.size, 0)
    }
  }
}

// Example usage and server setup
/*
import express from 'express'

const app = express()
const activityPub = new ActivityPubHelloWorld('example.com')

app.use(express.json({ type: 'application/activity+json' }))
app.use(express.json({ type: 'application/ld+json' }))

// WebFinger endpoint
app.get('/.well-known/webfinger', (req, res) => {
  const resource = req.query.resource as string
  const webfinger = activityPub.handleWebfinger(resource)

  if (webfinger) {
    res.json(webfinger)
  } else {
    res.status(404).json({ error: 'Not found' })
  }
})

// Actor profile endpoint
app.get('/users/:username', (req, res) => {
  const actorId = `https://example.com/users/${req.params.username}`
  const actor = activityPub.handleActorRequest(actorId)

  if (actor) {
    res.json(actor)
  } else {
    res.status(404).json({ error: 'Actor not found' })
  }
})

// Actor inbox
app.post('/users/:username/inbox', async (req, res) => {
  const actorId = `https://example.com/users/${req.params.username}`

  try {
    await activityPub.handleInboxRequest(actorId, req.body)
    res.status(200).send()
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// Actor outbox
app.get('/users/:username/outbox', async (req, res) => {
  const actorId = `https://example.com/users/${req.params.username}`

  try {
    const activities = await activityPub.getOutbox(actorId)
    res.json({
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: `${actorId}/outbox`,
      type: 'OrderedCollection',
      totalItems: activities.length,
      orderedItems: activities
    })
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// Server endpoints
app.get('/server/stats', (req, res) => {
  res.json(activityPub.getStatistics())
})

// Create new actor
app.post('/server/actors', async (req, res) => {
  try {
    const { username, name, summary } = req.body
    const actor = await activityPub.createActor(username, name, summary)
    res.status(201).json(actor)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// Create a note
app.post('/server/note', async (req, res) => {
  try {
    const { actorId, content, to } = req.body
    const activity = await activityPub.createNote(actorId, content, to)
    res.status(201).json(activity)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

// Follow another actor
app.post('/server/follow', async (req, res) => {
  try {
    const { actorId, targetActorId } = req.body
    const activity = await activityPub.followActor(actorId, targetActorId)
    res.status(201).json(activity)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

app.listen(3000, () => {
  console.log('ActivityPub server running on port 3000')
})
*/

// Client example for interacting with ActivityPub
class ActivityPubClient {
  constructor(private domain: string) {}

  async discoverActor(username: string): Promise<ActivityPubActor | null> {
    try {
      // WebFinger discovery
      const webfingerResponse = await fetch(
        `https://${this.domain}/.well-known/webfinger?resource=acct:${username}@${this.domain}`
      )

      if (!webfingerResponse.ok) {
        return null
      }

      const webfinger = await webfingerResponse.json()
      const selfLink = webfinger.links.find((link: any) => link.rel === 'self')

      if (!selfLink) {
        return null
      }

      // Fetch actor profile
      const actorResponse = await fetch(selfLink.href, {
        headers: {
          'Accept': 'application/activity+json'
        }
      })

      if (!actorResponse.ok) {
        return null
      }

      return await actorResponse.json()
    } catch (error) {
      console.error('Error discovering actor:', error)
      return null
    }
  }

  async sendFollow(actorId: string, targetActorId: string, privateKeyPem: string): Promise<void> {
    const followActivity = {
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: `https://${this.domain}/activities/follow/${Date.now()}`,
      type: 'Follow',
      actor: actorId,
      object: targetActorId
    }

    await this.deliverActivity(followActivity, privateKeyPem)
  }

  async createNote(actorId: string, content: string, privateKeyPem: string): Promise<void> {
    const noteId = `https://${this.domain}/objects/note/${Date.now()}`
    const activityId = `https://${this.domain}/activities/create/${Date.now()}`

    const createActivity = {
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: activityId,
      type: 'Create',
      actor: actorId,
      object: {
        id: noteId,
        type: 'Note',
        attributedTo: actorId,
        content,
        to: ['https://www.w3.org/ns/activitystreams#Public']
      }
    }

    await this.deliverActivity(createActivity, privateKeyPem)
  }

  private async deliverActivity(activity: any, privateKeyPem: string): Promise<void> {
    // In a real implementation:
    // 1. Sign the HTTP Signature using privateKeyPem
    // 2. Send to target's inbox
    // 3. Handle delivery status

    console.log('Delivering activity:', JSON.stringify(activity, null, 2))
  }
}

// Usage example:
const server = new ActivityPubHelloWorld('example.com')
const client = new ActivityPubClient('mastodon.social')

// Create some actors
const alice = await server.createActor('alice', 'Alice Smith', 'Developer and tech enthusiast')
const bob = await server.createActor('bob', 'Bob Johnson', 'Designer')

// Create a note
const note = await server.createNote(alice.id, 'Hello federated world! #ActivityPub')

// Follow another user
const follow = await server.followActor(bob.id, alice.id)

// Accept the follow
const accept = await server.acceptFollow(alice.id, follow.id)

// Like the note
const like = await server.likeObject(bob.id, note.id)

// Get statistics
console.log(server.getStatistics())

export default ActivityPubHelloWorld

💻 Couche de Compatibilité Mastodon typescript

🟡 intermediate ⭐⭐⭐⭐

Créer des instances compatibles Mastodon avec support ActivityPub complet, timelines et fédération

⏱️ 40 min 🏷️ activitypub, mastodon, federated, social
Prerequisites: ActivityPub basics, Mastodon API, HTTP protocols, Web standards
// Mastodon Compatibility Layer - ActivityPub Implementation
// Full Mastodon API compatibility with federation support

interface MastodonStatus {
  id: string
  created_at: string
  in_reply_to_id?: string
  in_reply_to_account_id?: string
  sensitive: boolean
  spoiler_text?: string
  visibility: 'public' | 'unlisted' | 'private' | 'direct'
  language?: string
  uri: string
  url: string
  replies_count: number
  reblogs_count: number
  favourites_count: number
  reblogged?: boolean
  favourited?: boolean
  muted?: boolean
  bookmarked?: boolean
  content: string
  reblog?: MastodonStatus
  application?: {
    name: string
    website?: string
  }
  account: MastodonAccount
  media_attachments?: MastodonMedia[]
  mentions?: MastodonMention[]
  tags?: MastodonTag[]
  emojis?: MastodonEmoji[]
  card?: MastodonCard
  poll?: MastodonPoll
}

interface MastodonAccount {
  id: string
  username: string
  acct: string
  display_name: string
  locked: boolean
  created_at: string
  followers_count: number
  following_count: number
  statuses_count: number
  note: string
  url: string
  avatar: string
  avatar_static: string
  header: string
  header_static: string
  emojis: MastodonEmoji[]
  moved?: boolean
  fields: Array<{
    name: string
    value: string
    verified_at?: string
  }>
  bot: boolean
  discoverable?: boolean
}

interface MastodonMedia {
  id: string
  type: 'image' | 'video' | 'gifv' | 'audio'
  url: string
  preview_url: string
  remote_url?: string
  preview_remote_url?: string
  text_url?: string
  meta: {
    original: {
      width: number
      height: number
      size: string
      aspect: number
    }
    small: {
      width: number
      height: number
      size: string
      aspect: number
    }
  }
  description?: string
  blurhash?: string
}

interface MastodonMention {
  id: string
  username: string
  url: string
  acct: string
}

interface MastodonTag {
  name: string
  url: string
  history?: Array<{
    day: string
    uses: string
    accounts: string
  }>
}

interface MastodonEmoji {
  shortcode: string
  url: string
  static_url: string
  visible_in_picker: boolean
  category?: string
}

interface MastodonCard {
  url: string
  title: string
  description: string
  image?: string
  type: 'link' | 'photo' | 'video' | 'rich'
  author_name?: string
  author_url?: string
  provider_name?: string
  provider_url?: string
  html?: string
  width?: number
  height?: number
  image?: string
  embed_url?: string
  blurhash?: string
}

interface MastodonPoll {
  id: string
  expires_at: string
  expired: boolean
  multiple: boolean
  votes_count: number
  voted: boolean
  options: Array<{
    title: string
    votes_count: number
  }>
  emojis: MastodonEmoji[]
}

interface MastodonTimeline {
  id: string
  type: 'home' | 'local' | 'federated' | 'public'
  items: MastodonStatus[]
}

class MastodonCompatibilityLayer {
  private activityPub: ActivityPubHelloWorld
  private statusMap = new Map<string, MastodonStatus>()
  private accountMap = new Map<string, MastodonAccount>()
  private timelines = new Map<string, MastodonTimeline>()
  private notifications = new Map<string, MastodonNotification[]>()

  constructor(private instanceDomain: string) {
    this.activityPub = new ActivityPubHelloWorld(instanceDomain)
  }

  // Convert ActivityPub to Mastodon format
  private async convertActivityToStatus(activity: Activity): Promise<MastodonStatus> {
    const object = typeof activity.object === 'string'
      ? await this.activityPub['objects'].get(activity.object)
      : activity.object as ActivityPubObject

    if (!object) {
      throw new Error('Object not found')
    }

    const actor = await this.activityPub['actors'].get(activity.actor)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Handle boosts/reblogs
    if (activity.type === 'Announce') {
      const originalStatus = await this.convertObjectToStatus(object as ActivityPubObject)
      return {
        id: activity.id,
        created_at: activity.published,
        reblog: originalStatus,
        account: await this.convertActorToAccount(actor),
        reblogged: true,
        replies_count: 0,
        reblogs_count: 0,
        favourites_count: 0,
        sensitive: false,
        visibility: 'public',
        language: 'en',
        uri: activity.id,
        url: activity.id,
        content: `Hello from ActivityPub!`,
        note: `Status note`,
        emojis: [],
        media_attachments: [],
        mentions: [],
        tags: []
      }
    }

    // Handle likes
    if (activity.type === 'Like') {
      throw new Error('Like activities cannot be converted to status')
    }

    return await this.convertObjectToStatus(object)
  }

  private async convertObjectToStatus(object: ActivityPubObject): Promise<MastodonStatus> {
    const actor = await this.activityPub['actors'].get(object.attributedTo)
    if (!actor) {
      throw new Error('Actor not found')
    }

    // Extract content and mentions
    const { content, mentions, tags } = this.parseContent(object.content)

    // Determine visibility
    const visibility = this.determineVisibility(object.to, object.cc)

    // Convert attachments
    const mediaAttachments = object.attachment ?
      object.attachment.map(this.convertMediaAttachment) : []

    return {
      id: object.id,
      created_at: object.published,
      in_reply_to_id: this.getInReplyToId(object),
      sensitive: false,
      spoiler_text: '',
      visibility,
      language: 'en',
      uri: object.url || object.id,
      url: object.url || object.id,
      replies_count: 0, // Would need to count replies
      reblogs_count: 0, // Would need to count announces
      favourites_count: 0, // Would need to count likes
      content,
      account: await this.convertActorToAccount(actor),
      emojis: [],
      media_attachments: mediaAttachments,
      mentions,
      tags: tags.map(tag => ({
        name: tag.name,
        url: `https://${this.instanceDomain}/tags/${tag.name}`
      }))
    }
  }

  private async convertActorToAccount(actor: ActivityPubActor): Promise<MastodonAccount> {
    return {
      id: actor.id,
      username: actor.preferredUsername,
      acct: `${actor.preferredUsername}@${new URL(actor.id).hostname}`,
      display_name: actor.name,
      locked: false,
      created_at: new Date().toISOString(), // Would need to store creation time
      followers_count: this.activityPub['followers'].get(actor.id)?.size || 0,
      following_count: this.activityPub['following'].get(actor.id)?.size || 0,
      statuses_count: this.activityPub['activities'].values()
        .filter(a => a.actor === actor.id && a.type === 'Create').length,
      note: actor.summary || '',
      url: actor.id,
      avatar: actor.icon?.url || '',
      avatar_static: actor.icon?.url || '',
      header: actor.image?.url || '',
      header_static: actor.image?.url || '',
      emojis: [],
      bot: false,
      discoverable: true,
      fields: []
    }
  }

  private convertMediaAttachment(attachment: any): MastodonMedia {
    return {
      id: attachment.id,
      type: attachment.mediaType || 'image',
      url: attachment.url,
      preview_url: attachment.url,
      description: attachment.name,
      meta: {
        original: {
          width: attachment.width || 800,
          height: attachment.height || 600,
          size: 'unknown',
          aspect: (attachment.width || 800) / (attachment.height || 600)
        },
        small: {
          width: 100,
          height: 75,
          size: 'unknown',
          aspect: 4/3
        }
      }
    }
  }

  private parseContent(content: string): {
    content: string
    mentions: MastodonMention[]
    tags: MastodonTag[]
  } {
    // Extract mentions from content
    const mentionRegex = /<a href="([^"]+)"[^>]*>@([^<]+)<\/a>/g
    const mentions: MastodonMention[] = []
    let mentionMatch

    while ((mentionMatch = mentionRegex.exec(content)) !== null) {
      const url = mentionMatch[1]
      const username = mentionMatch[2]

      mentions.push({
        id: url,
        username,
        url,
        acct: username
      })
    }

    // Extract hashtags from content
    const hashtagRegex = /<a href="([^"]+)"[^>]*>#([^<]+)<\/a>/g
    const tags: MastodonTag[] = []
    let hashtagMatch

    while ((hashtagMatch = hashtagRegex.exec(content)) !== null) {
      const url = hashtagMatch[1]
      const name = hashtagMatch[2]

      tags.push({
        name,
        url
      })
    }

    // Convert to plain text (simplified)
    const plainContent = content
      .replace(/<a href="[^"]+"[^>]*>@([^<]+)<\/a>/g, '@$1')
      .replace(/<a href="[^"]+"[^>]*>#([^<]+)<\/a>/g, '#$1')
      .replace(/<[^>]*>/g, '')

    return {
      content: plainContent,
      mentions,
      tags
    }
  }

  private determineVisibility(to: string[], cc?: string[]): MastodonStatus['visibility'] {
    if (to?.includes('https://www.w3.org/ns/activitystreams#Public')) {
      return 'public'
    }
    if (cc?.includes('https://www.w3.org/ns/activitystreams#Public')) {
      return 'unlisted'
    }
    return 'private'
  }

  private getInReplyToId(object: ActivityPubObject): string | undefined {
    // This would need to be stored when creating replies
    return undefined
  }

  // Mastodon API compatibility methods

  // GET /api/v1/statuses/:id
  async getStatus(id: string): Promise<MastodonStatus> {
    // Check if we have this status cached
    let status = this.statusMap.get(id)

    if (!status) {
      // Try to find the activity that created this object
      const activity = Array.from(this.activityPub['activities'].values())
        .find(a => a.type === 'Create' &&
                    typeof a.object === 'object' &&
                    (a.object as ActivityPubObject).id === id)

      if (activity) {
        status = await this.convertActivityToStatus(activity)
        this.statusMap.set(id, status)
      }
    }

    if (!status) {
      throw new Error('Status not found')
    }

    return status
  }

  // POST /api/v1/statuses
  async createStatus(params: {
    status: string
    in_reply_to_id?: string
    sensitive?: boolean
    spoiler_text?: string
    visibility?: MastodonStatus['visibility']
    media_ids?: string[]
    language?: string
  }, actorId: string): Promise<MastodonStatus> {
    // Convert Mastodon parameters to ActivityPub
    const to = this.visibilityToRecipients(params.visibility || 'public')
    const content = this.addActivityPubFormatting(params.status)

    // Handle reply
    if (params.in_reply_to_id) {
      const replyToStatus = await this.getStatus(params.in_reply_to_id)
      to.push(replyToStatus.account.acct)
    }

    // Create the note
    const activity = await this.activityPub.createNote(actorId, content, to)

    // Convert to Mastodon format
    const status = await this.convertActivityToStatus(activity)

    // Add additional Mastodon-specific fields
    if (params.spoiler_text) {
      status.spoiler_text = params.spoiler_text
    }

    this.statusMap.set(status.id, status)
    return status
  }

  // DELETE /api/v1/statuses/:id
  async deleteStatus(id: string): Promise<MastodonStatus> {
    // Find the Create activity for this status
    const createActivity = Array.from(this.activityPub['activities'].values())
      .find(a => a.type === 'Create' &&
                    typeof a.object === 'object' &&
                    (a.object as ActivityPubObject).id === id)

    if (!createActivity) {
      throw new Error('Status not found')
    }

    // Create Delete activity
    const deleteActivity = await this.activityPub['undoActivity'](createActivity.actor, createActivity.id)

    // Remove from cache
    this.statusMap.delete(id)

    // Return the deleted status
    return await this.getStatus(id)
  }

  // POST /api/v1/statuses/:id/reblog
  async reblogStatus(id: string, actorId: string): Promise<MastodonStatus> {
    const activity = await this.activityPub.announceObject(actorId, id)
    const status = await this.convertActivityToStatus(activity)

    // Update reblogged status
    const originalStatus = await this.getStatus(id)
    originalStatus.reblogs_count++

    this.statusMap.set(status.id, status)
    this.statusMap.set(originalStatus.id, originalStatus)

    return status
  }

  // POST /api/v1/statuses/:id/favourite
  async favouriteStatus(id: string, actorId: string): Promise<MastodonStatus> {
    const activity = await this.activityPub.likeObject(actorId, id)
    const status = await this.getStatus(id)

    // Update favourite count
    status.favourites_count++
    status.favourited = true

    this.statusMap.set(status.id, status)

    return status
  }

  // GET /api/v1/accounts/:id
  async getAccount(id: string): Promise<MastodonAccount> {
    let account = this.accountMap.get(id)

    if (!account) {
      const actor = this.activityPub['actors'].get(id)
      if (actor) {
        account = await this.convertActorToAccount(actor)
        this.accountMap.set(id, account)
      }
    }

    if (!account) {
      throw new Error('Account not found')
    }

    return account
  }

  // GET /api/v1/accounts/verify_credentials
  async verifyCredentials(actorId: string): Promise<MastodonAccount> {
    return await this.getAccount(actorId)
  }

  // GET /api/v1/timelines/home
  async getHomeTimeline(actorId: string, options: {
    max_id?: string
    since_id?: string
    limit?: number
  } = {}): Promise<MastodonStatus[]> {
    const inbox = await this.activityPub.getInbox(actorId)

    // Convert activities to statuses
    const statuses: MastodonStatus[] = []
    for (const activity of inbox) {
      try {
        if (activity.type === 'Create') {
          const status = await this.convertActivityToStatus(activity)
          if (!status.reblog) { // Don't include boosts in home timeline
            statuses.push(status)
          }
        } else if (activity.type === 'Announce') {
          const status = await this.convertActivityToStatus(activity)
          if (status.reblog) {
            statuses.push(status)
          }
        }
      } catch (error) {
        console.warn('Error converting activity to status:', error)
      }
    }

    // Apply pagination filters
    let filteredStatuses = statuses.sort((a, b) =>
      new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
    )

    if (options.max_id) {
      const maxStatus = await this.getStatus(options.max_id)
      const maxDate = new Date(maxStatus.created_at)
      filteredStatuses = filteredStatuses.filter(s =>
        new Date(s.created_at) < maxDate
      )
    }

    if (options.since_id) {
      const sinceStatus = await this.getStatus(options.since_id)
      const sinceDate = new Date(sinceStatus.created_at)
      filteredStatuses = filteredStatuses.filter(s =>
        new Date(s.created_at) > sinceDate
      )
    }

    if (options.limit) {
      filteredStatuses = filteredStatuses.slice(0, options.limit)
    }

    return filteredStatuses
  }

  // GET /api/v1/timelines/public
  async getPublicTimeline(options: {
    local?: boolean
    remote?: boolean
    only_media?: boolean
    max_id?: string
    since_id?: string
    limit?: number
  } = {}): Promise<MastodonStatus[]> {
    // Get all public activities
    const allActivities = Array.from(this.activityPub['activities'].values())
      .filter(activity => activity.to?.includes('https://www.w3.org/ns/activitystreams#Public'))

    const statuses: MastodonStatus[] = []

    for (const activity of allActivities) {
      try {
        if (activity.type === 'Create') {
          const status = await this.convertActivityToStatus(activity)

          // Apply filters
          if (options.local && !status.account.acct.endsWith(`@${this.instanceDomain}`)) {
            continue
          }
          if (options.remote && status.account.acct.endsWith(`@${this.instanceDomain}`)) {
            continue
          }
          if (options.only_media && status.media_attachments.length === 0) {
            continue
          }

          statuses.push(status)
        }
      } catch (error) {
        console.warn('Error converting activity to status:', error)
      }
    }

    // Sort and paginate
    let filteredStatuses = statuses.sort((a, b) =>
      new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
    )

    if (options.limit) {
      filteredStatuses = filteredStatuses.slice(0, options.limit)
    }

    return filteredStatuses
  }

  // POST /api/v1/accounts/:id/follow
  async followAccount(actorId: string, targetAccountId: string): Promise<MastodonRelationship> {
    const activity = await this.activityPub.followActor(actorId, targetAccountId)

    return {
      id: targetAccountId,
      following: true,
      showing_reblogs: true,
      notifying: false,
      blocked: false,
      muting: false,
      muting_notifications: false,
      requested: false,
      domain_blocking: false
    }
  }

  // POST /api/v1/accounts/:id/unfollow
  async unfollowAccount(actorId: string, targetAccountId: string): Promise<MastodonRelationship> {
    // Find the follow activity
    const followActivity = Array.from(this.activityPub['activities'].values())
      .find(a => a.type === 'Follow' && a.actor === actorId && a.object === targetAccountId)

    if (followActivity) {
      await this.activityPub.undoActivity(actorId, followActivity.id)
    }

    return {
      id: targetAccountId,
      following: false,
      showing_reblogs: true,
      notifying: false,
      blocked: false,
      muting: false,
      muting_notifications: false,
      requested: false,
      domain_blocking: false
    }
  }

  // GET /api/v1/accounts/:id/followers
  async getFollowers(id: string, options: { limit?: number } = {}): Promise<MastodonAccount[]> {
    const followersSet = this.activityPub['followers'].get(id) || new Set()
    const accounts: MastodonAccount[] = []

    for (const followerId of followersSet) {
      const account = await this.getAccount(followerId)
      accounts.push(account)
    }

    // Sort by relationship creation time (would need to store this)
    if (options.limit) {
      return accounts.slice(0, options.limit)
    }

    return accounts
  }

  // GET /api/v1/accounts/:id/following
  async getFollowing(id: string, options: { limit?: number } = {}): Promise<MastodonAccount[]> {
    const followingSet = this.activityPub['following'].get(id) || new Set()
    const accounts: MastodonAccount[] = []

    for (const followingId of followingSet) {
      const account = await this.getAccount(followingId)
      accounts.push(account)
    }

    if (options.limit) {
      return accounts.slice(0, options.limit)
    }

    return accounts
  }

  // Utility methods
  private visibilityToRecipients(visibility: string): string[] {
    switch (visibility) {
      case 'public':
        return ['https://www.w3.org/ns/activitystreams#Public']
      case 'unlisted':
        return []
      case 'private':
        return [] // Would need to add followers
      case 'direct':
        return [] // Would need to add specific recipients
      default:
        return ['https://www.w3.org/ns/activitystreams#Public']
    }
  }

  private addActivityPubFormatting(text: string): string {
    // Convert hashtags to ActivityPub format
    let formatted = text.replace(/#(\w+)/g, '<a href="https://\' + this.instanceDomain + '/tags/$1" rel="tag">#$1</a>')

    // Convert mentions to ActivityPub format (simplified)
    formatted = formatted.replace(/@([\w.-]+@[\w.-]+)/g, (match, acct) => {
      const [username, domain] = acct.split('@')
      return `<span class="h-card"><a href="https://${domain}/@${username}" class="u-url mention">${match}</a></span>`
    })

    // Convert line breaks
    formatted = formatted.replace(/\n/g, '<br>')

    return formatted
  }

  // Server setup for Mastodon API
  createServer() {
    const express = require('express')
    const app = express()

    app.use(express.json())

    // Status endpoints
    app.get('/api/v1/statuses/:id', async (req, res) => {
      try {
        const status = await this.getStatus(req.params.id)
        res.json(status)
      } catch (error) {
        res.status(404).json({ error: error.message })
      }
    })

    app.post('/api/v1/statuses', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const status = await this.createStatus(req.body, actorId)
        res.status(201).json(status)
      } catch (error) {
        res.status(400).json({ error: error.message })
      }
    })

    app.delete('/api/v1/statuses/:id', async (req, res) => {
      try {
        const status = await this.deleteStatus(req.params.id)
        res.json(status)
      } catch (error) {
        res.status(404).json({ error: error.message })
      }
    })

    app.post('/api/v1/statuses/:id/reblog', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const status = await this.reblogStatus(req.params.id, actorId)
        res.json(status)
      } catch (error) {
        res.status(422).json({ error: error.message })
      }
    })

    app.post('/api/v1/statuses/:id/favourite', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const status = await this.favouriteStatus(req.params.id, actorId)
        res.json(status)
      } catch (error) {
        res.status(422).json({ error: error.message })
      }
    })

    // Account endpoints
    app.get('/api/v1/accounts/:id', async (req, res) => {
      try {
        const account = await this.getAccount(req.params.id)
        res.json(account)
      } catch (error) {
        res.status(404).json({ error: error.message })
      }
    })

    app.get('/api/v1/accounts/verify_credentials', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const account = await this.verifyCredentials(actorId)
        res.json(account)
      } catch (error) {
        res.status(401).json({ error: 'Unauthorized' })
      }
    })

    app.post('/api/v1/accounts/:id/follow', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const relationship = await this.followAccount(actorId, req.params.id)
        res.json(relationship)
      } catch (error) {
        res.status(422).json({ error: error.message })
      }
    })

    app.post('/api/v1/accounts/:id/unfollow', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const relationship = await this.unfollowAccount(actorId, req.params.id)
        res.json(relationship)
      } catch (error) {
        res.status(422).json({ error: error.message })
      }
    })

    app.get('/api/v1/accounts/:id/followers', async (req, res) => {
      try {
        const accounts = await this.getFollowers(req.params.id, req.query)
        res.json(accounts)
      } catch (error) {
        res.status(404).json({ error: error.message })
      }
    })

    app.get('/api/v1/accounts/:id/following', async (req, res) => {
      try {
        const accounts = await this.getFollowing(req.params.id, req.query)
        res.json(accounts)
      } catch (error) {
        res.status(404).json({ error: error.message })
      }
    })

    // Timeline endpoints
    app.get('/api/v1/timelines/home', async (req, res) => {
      try {
        const actorId = req.headers['x-actor-id'] as string
        const timeline = await this.getHomeTimeline(actorId, req.query)
        res.json(timeline)
      } catch (error) {
        res.status(400).json({ error: error.message })
      }
    })

    app.get('/api/v1/timelines/public', async (req, res) => {
      try {
        const timeline = await this.getPublicTimeline(req.query)
        res.json(timeline)
      } catch (error) {
        res.status(400).json({ error: error.message })
      }
    })

    return app
  }
}

// Additional interfaces
interface MastodonNotification {
  id: string
  type: 'mention' | 'reblog' | 'favourite' | 'follow' | 'poll' | 'follow_request'
  created_at: string
  account: MastodonAccount
  status?: MastodonStatus
}

interface MastodonRelationship {
  id: string
  following: boolean
  showing_reblogs: boolean
  notifying: boolean
  blocked: boolean
  muting: boolean
  muting_notifications: boolean
  requested: boolean
  domain_blocking: boolean
}

// Usage example:
/*
const mastodon = new MastodonCompatibilityLayer('mastodon.example.com')
const app = mastodon.createServer()

app.listen(3000, () => {
  console.log('Mastodon-compatible server running on port 3000')
})

// Create an actor
await mastodon.activityPub.createActor('alice', 'Alice Smith', 'Developer')

// Create a post
const status = await mastodon.createStatus({
  status: 'Hello from our Mastodon-compatible instance! #ActivityPub',
  visibility: 'public'
}, 'https://mastodon.example.com/users/alice')
*/

export default MastodonCompatibilityLayer

💻 Routeur de Fédération et Relay typescript

🔴 complex ⭐⭐⭐⭐⭐

Routeur de fédération avancé avec services de relay, filtrage de contenu et communication inter-instances

⏱️ 50 min 🏷️ activitypub, federation, relay, distributed
Prerequisites: Advanced ActivityPub, HTTP protocols, Content filtering, Distributed systems
// ActivityPub Federation Router and Relay
// Advanced federation with content filtering, relays, and cross-instance communication

interface FederationInstance {
  domain: string
  software: string
  version: string
  openRegistrations: boolean
  approvalRequired: boolean
  inviteOnly: boolean
  users: number
  statusCount: number
  lastActive: Date
  adminEmail?: string
  contactAccount?: string
  rules: FederationRule[]
}

interface FederationRule {
  id: string
  type: 'content' | 'user' | 'instance' | 'interaction'
  action: 'allow' | 'block' | 'filter' | 'report'
  conditions: {
    content?: string[]
    users?: string[]
    instances?: string[]
    interactionTypes?: string[]
    severity?: 'low' | 'medium' | 'high'
  }
  actions: {
    blockDelivery?: boolean
    quarantineContent?: boolean
    notifyModerators?: boolean
    addWarning?: boolean
    rateLimit?: number
  }
}

interface FederationStats {
  totalInstances: number
  activeInstances: number
  totalUsers: number
  totalPosts: number
  messagesDelivered: number
  messagesReceived: number
  deliveryFailures: number
  blockedInstances: number
  topInstances: Array<{
    domain: string
    messages: number
    lastActivity: Date
  }>
}

interface RelaySubscription {
  id: string
  domain: string
  subscribed: boolean
  filters: string[]
  priority: number
  createdAt: Date
  lastSync: Date
}

interface ContentFilter {
  id: string
  name: string
  type: 'keyword' | 'regex' | 'ml' | 'domain' | 'language'
  pattern: string
  action: 'block' | 'filter' | 'flag' | 'transform'
  enabled: boolean
  severity: number
  falsePositiveRate: number
  lastUpdated: Date
}

interface FederationMessage {
  id: string
  type: Activity['type']
  actor: string
  object: any
  target?: string
  to: string[]
  cc: string[]
  origin: string
  timestamp: Date
  retries: number
  delivered: boolean
  blocked: boolean
  processed: boolean
  filterResults: FilterResult[]
}

interface FilterResult {
  filterId: string
  filterName: string
  matched: boolean
  score: number
  action: string
  reason?: string
}

class ActivityPubFederationRouter {
  private instances: Map<string, FederationInstance> = new Map()
  private blockedInstances: Set<string> = new Set()
  private contentFilters: Map<string, ContentFilter> = new Map()
  private relaySubscriptions: Map<string, RelaySubscription> = new Map()
  private messageQueue: FederationMessage[] = []
  private stats: FederationStats
  private deliveryQueue: Map<string, FederationMessage[]> = new Map()

  constructor(
    private localDomain: string,
    private options: {
      enableRelay?: boolean
      enableContentFiltering?: boolean
      enableRateLimiting?: boolean
      maxRetries?: number
      batchSize?: number
      relayEndpoints?: string[]
      defaultFilters?: ContentFilter[]
    } = {}
  ) {
    this.stats = {
      totalInstances: 0,
      activeInstances: 0,
      totalUsers: 0,
      totalPosts: 0,
      messagesDelivered: 0,
      messagesReceived: 0,
      deliveryFailures: 0,
      blockedInstances: 0,
      topInstances: []
    }

    this.setupDefaultFilters()
    this.startBackgroundWorkers()
  }

  // Setup default content filters
  private setupDefaultFilters(): void {
    const defaultFilters: ContentFilter[] = [
      {
        id: 'spam-keywords',
        name: 'Spam Keywords',
        type: 'keyword',
        pattern: 'viagra,casino,lottery,free money,click here',
        action: 'block',
        enabled: true,
        severity: 8,
        falsePositiveRate: 0.02,
        lastUpdated: new Date()
      },
      {
        id: 'harmful-content',
        name: 'Harmful Content',
        type: 'regex',
        pattern: '\b(hate|violence|threat|abuse)\b',
        action: 'filter',
        enabled: true,
        severity: 9,
        falsePositiveRate: 0.05,
        lastUpdated: new Date()
      },
      {
        id: 'spam-patterns',
        name: 'Spam Patterns',
        type: 'regex',
        pattern: '([a-zA-Z])\1{4,}|\b(\w+)\2{4,}',
        action: 'filter',
        enabled: true,
        severity: 7,
        falsePositiveRate: 0.1,
        lastUpdated: new Date()
      },
      {
        id: 'spam-domains',
        name: 'Known Spam Domains',
        type: 'domain',
        pattern: 'spam.example,fake-site.example',
        action: 'block',
        enabled: true,
        severity: 10,
        falsePositiveRate: 0.01,
        lastUpdated: new Date()
      }
    ]

    for (const filter of defaultFilters) {
      this.contentFilters.set(filter.id, filter)
    }
  }

  // Discover and register a new instance
  async discoverInstance(domain: string): Promise<FederationInstance> {
    console.log(`Discovering instance: ${domain}`)

    try {
      // Check if instance already exists
      if (this.instances.has(domain)) {
        return this.instances.get(domain)!
      }

      // Fetch instance information
      const instanceInfo = await this.fetchInstanceInfo(domain)

      // Create instance record
      const instance: FederationInstance = {
        domain,
        software: instanceInfo.software,
        version: instanceInfo.version,
        openRegistrations: instanceInfo.registrations?.open || false,
        approvalRequired: instanceInfo.registrations?.approval_required || false,
        inviteOnly: instanceInfo.registrations?.approval_required || false,
        users: instanceInfo.usage?.users?.total_count || 0,
        statusCount: instanceInfo.usage?.local_posts || 0,
        lastActive: new Date(),
        adminEmail: instanceInfo.contact?.email,
        contactAccount: instanceInfo.contact?.account,
        rules: this.generateInstanceRules(instanceInfo)
      }

      // Store instance
      this.instances.set(domain, instance)
      this.stats.totalInstances++
      this.stats.activeInstances++

      // Check for federation rules
      await this.checkFederationRules(instance)

      console.log(`Discovered instance: ${domain} (${instance.software} ${instance.version})`)
      return instance

    } catch (error) {
      console.error(`Failed to discover instance ${domain}:`, error)
      throw error
    }
  }

  // Fetch instance information via NodeInfo
  private async fetchInstanceInfo(domain: string): Promise<any> {
    try {
      // Try NodeInfo 2.0 first
      const nodeInfo2Response = await fetch(`https://${domain}/.well-known/nodeinfo`)
      if (nodeInfo2Response.ok) {
        const nodeInfo2 = await nodeInfo2Response.json()
        if (nodeInfo2.links && nodeInfo2.links.length > 0) {
          const nodeInfoUrl = nodeInfo2.links[0].href
          const response = await fetch(nodeInfoUrl)
          if (response.ok) {
            return await response.json()
          }
        }
      }

      // Fallback to NodeInfo 1.0
      const nodeInfo1Response = await fetch(`https://${domain}/.well-known/nodeinfo`)
      if (nodeInfo1Response.ok) {
        return await nodeInfo1Response.json()
      }

      throw new Error('NodeInfo not found')

    } catch (error) {
      console.warn(`Failed to fetch NodeInfo for ${domain}:`, error)
      return {
        software: 'unknown',
        version: 'unknown',
        registrations: { open: false }
      }
    }
  }

  // Generate instance rules based on instance info
  private generateInstanceRules(instanceInfo: any): FederationRule[] {
    const rules: FederationRule[] = []

    // Rule for closed instances
    if (!instanceInfo.registrations?.open) {
      rules.push({
        id: `closed-instance-${instanceInfo.domain}`,
        type: 'instance',
        action: 'filter',
        conditions: {
          instances: [instanceInfo.domain]
        },
        actions: {
          blockDelivery: false,
          quarantineContent: true,
          notifyModerators: true
        }
      })
    }

    // Rule for instances with many users (potential spam)
    if (instanceInfo.usage?.users?.total_count > 1000000) {
      rules.push({
        id: `high-activity-instance-${instanceInfo.domain}`,
        type: 'instance',
        action: 'filter',
        conditions: {
          instances: [instanceInfo.domain]
        },
        actions: {
          rateLimit: 10,
          quarantineContent: false
        }
      })
    }

    return rules
  }

  // Check federation rules against instance
  private async checkFederationRules(instance: FederationInstance): Promise<void> {
    for (const rule of this.globalFederationRules) {
      if (this.evaluateRule(rule, instance)) {
        await this.applyFederationRule(rule, instance)
      }
    }
  }

  // Evaluate federation rule
  private evaluateRule(rule: FederationRule, instance: FederationInstance): boolean {
    const conditions = rule.conditions

    if (conditions.instances && conditions.instances.includes(instance.domain)) {
      return true
    }

    if (conditions.software && conditions.software.includes(instance.software)) {
      return true
    }

    if (conditions.users) {
      if (conditions.users.min && instance.users < conditions.users.min) return false
      if (conditions.users.max && instance.users > conditions.users.max) return false
    }

    return false
  }

  // Apply federation rule
  private async applyFederationRule(rule: FederationRule, instance: FederationInstance): Promise<void> {
    if (rule.actions.block) {
      this.blockInstance(instance.domain)
    }

    if (rule.actions.quarantine) {
      // Add to quarantine list
      console.log(`Quarantined instance: ${instance.domain}`)
    }

    if (rule.actions.notifyAdmins) {
      // Notify administrators
      console.log(`Notifying admins about instance: ${instance.domain}`)
    }
  }

  // Block an instance
  blockInstance(domain: string): void {
    this.blockedInstances.add(domain)
    this.stats.blockedInstances++
    this.stats.activeInstances--
    console.log(`Blocked instance: ${domain}`)
  }

  // Unblock an instance
  unblockInstance(domain: string): void {
    if (this.blockedInstances.delete(domain)) {
      this.stats.blockedInstances--
      if (this.instances.has(domain)) {
        this.stats.activeInstances++
      }
      console.log(`Unblocked instance: ${domain}`)
    }
  }

  // Process incoming federation message
  async processMessage(message: Partial<FederationMessage>): Promise<{
    success: boolean
    blocked: boolean
    filtered: boolean
    results: FilterResult[]
  }> {
    const federationMessage: FederationMessage = {
      id: message.id || this.generateMessageId(),
      type: message.type || 'Create',
      actor: message.actor || '',
      object: message.object,
      target: message.target,
      to: message.to || [],
      cc: message.cc || [],
      origin: message.origin || new URL(message.actor || '').hostname,
      timestamp: message.timestamp || new Date(),
      retries: message.retries || 0,
      delivered: false,
      blocked: false,
      processed: false,
      filterResults: []
    }

    this.stats.messagesReceived++

    // Check if origin is blocked
    if (this.blockedInstances.has(federationMessage.origin)) {
      federationMessage.blocked = true
      return {
        success: false,
        blocked: true,
        filtered: false,
        results: [{
          filterId: 'blocked-instance',
          filterName: 'Blocked Instance',
          matched: true,
          score: 100,
          action: 'block',
          reason: `Instance ${federationMessage.origin} is blocked`
        }]
      }
    }

    // Apply content filters
    const filterResults = await this.applyContentFilters(federationMessage)
    federationMessage.filterResults = filterResults

    const shouldBlock = filterResults.some(result => result.action === 'block')
    const shouldFilter = filterResults.some(result => result.action === 'filter')

    federationMessage.blocked = shouldBlock
    federationMessage.processed = true

    if (!shouldBlock && !shouldFilter) {
      // Queue for delivery
      await this.queueForDelivery(federationMessage)
    }

    return {
      success: !shouldBlock && !shouldFilter,
      blocked: shouldBlock,
      filtered: shouldFilter,
      results: filterResults
    }
  }

  // Apply content filters to message
  private async applyContentFilters(message: FederationMessage): Promise<FilterResult[]> {
    const results: FilterResult[] = []

    for (const [filterId, filter] of this.contentFilters) {
      if (!filter.enabled) continue

      const result = await this.applyContentFilter(filter, message)
      results.push(result)

      if (result.matched && result.action === 'block') {
        // Early exit on blocking filter
        break
      }
    }

    return results
  }

  // Apply individual content filter
  private async applyContentFilter(filter: ContentFilter, message: FederationMessage): Promise<FilterResult> {
    const content = this.extractContentForFiltering(message)
    let matched = false
    let score = 0

    switch (filter.type) {
      case 'keyword':
        matched = this.checkKeywordFilter(filter.pattern, content)
        score = matched ? filter.severity : 0
        break

      case 'regex':
        matched = this.checkRegexFilter(filter.pattern, content)
        score = matched ? filter.severity : 0
        break

      case 'domain':
        matched = this.checkDomainFilter(filter.pattern, message.actor)
        score = matched ? filter.severity : 0
        break

      case 'language':
        matched = this.checkLanguageFilter(filter.pattern, content)
        score = matched ? filter.severity : 0
        break

      case 'ml':
        // Machine learning filter (simplified)
        matched = await this.checkMLFilter(content)
        score = matched ? filter.severity : 0
        break
    }

    return {
      filterId,
      filterName: filter.name,
      matched,
      score,
      action: matched ? filter.action : 'allow',
      reason: matched ? `Matched ${filter.type} filter` : undefined
    }
  }

  // Extract content from message for filtering
  private extractContentForFiltering(message: FederationMessage): string {
    let content = ''

    if (typeof message.object === 'object') {
      const obj = message.object as any
      content += obj.content || ''
      content += obj.name || ''
    }

    content += message.type || ''
    content += message.actor || ''

    return content.toLowerCase()
  }

  // Check keyword filter
  private checkKeywordFilter(pattern: string, content: string): boolean {
    const keywords = pattern.split(',')
    return keywords.some(keyword => content.includes(keyword.trim().toLowerCase()))
  }

  // Check regex filter
  private checkRegexFilter(pattern: string, content: string): boolean {
    try {
      const regex = new RegExp(pattern, 'i')
      return regex.test(content)
    } catch (error) {
      console.warn(`Invalid regex pattern: ${pattern}`, error)
      return false
    }
  }

  // Check domain filter
  private checkDomainFilter(pattern: string, actorUrl: string): boolean {
    const domains = pattern.split(',')
    try {
      const actorDomain = new URL(actorUrl).hostname
      return domains.some(domain => actorDomain === domain.trim().toLowerCase())
    } catch (error) {
      return false
    }
  }

  // Check language filter
  private checkLanguageFilter(pattern: string, content: string): boolean {
    // Simplified language detection
    // In a real implementation, use a proper language detection library
    const suspiciousPatterns = ['\u4e00-\u9fff', '\u0600-\u06ff'] // Chinese, Arabic
    return suspiciousPatterns.some(pattern => {
      const regex = new RegExp(pattern)
      return regex.test(content)
    })
  }

  // Check ML filter
  private async checkMLFilter(content: string): Promise<boolean> {
    // Simplified ML-based spam detection
    const spamIndicators = [
      /[a-zA-Z]\1{4,}/, // Repeated characters
      /\b(buy now|click here|free money)\b/i, // Spam phrases
      content.length < 10, // Very short content
      content.length > 10000 // Very long content
    ]

    return spamIndicators.some(indicator => indicator)
  }

  // Queue message for delivery
  private async queueForDelivery(message: FederationMessage): Promise<void> {
    // Add to delivery queues for each recipient
    const recipients = [...message.to, ...message.cc]

    for (const recipient of recipients) {
      if (!this.deliveryQueue.has(recipient)) {
        this.deliveryQueue.set(recipient, [])
      }
      this.deliveryQueue.get(recipient)!.push(message)
    }

    // Also queue for relay subscriptions
    await this.queueForRelay(message)
  }

  // Queue for relay services
  private async queueForRelay(message: FederationMessage): Promise<void> {
    if (!this.options.enableRelay) return

    for (const [relayId, subscription] of this.relaySubscriptions) {
      if (!subscription.subscribed) continue

      // Check if message matches relay filters
      if (this.matchesRelayFilters(message, subscription.filters)) {
        if (!this.deliveryQueue.has(relayId)) {
          this.deliveryQueue.set(relayId, [])
        }
        this.deliveryQueue.get(relayId)!.push(message)
      }
    }
  }

  // Check if message matches relay filters
  private matchesRelayFilters(message: FederationMessage, filters: string[]): boolean {
    if (filters.length === 0) return true

    const content = this.extractContentForFiltering(message)
    return filters.some(filter => {
      try {
        return new RegExp(filter, 'i').test(content)
      } catch (error) {
        return false
      }
    })
  }

  // Start background workers
  private startBackgroundWorkers(): void {
    // Message delivery worker
    setInterval(() => {
      this.processDeliveryQueue()
    }, 5000) // Process every 5 seconds

    // Instance discovery worker
    setInterval(() => {
      this.discoverInstances()
    }, 60000) // Check for new instances every minute

    // Statistics update worker
    setInterval(() => {
      this.updateStatistics()
    }, 30000) // Update stats every 30 seconds

    // Health check worker
    setInterval(() => {
      this.performHealthChecks()
    }, 300000) // Health check every 5 minutes
  }

  // Process delivery queue
  private async processDeliveryQueue(): Promise<void> {
    const batchSize = this.options.batchSize || 10

    for (const [recipient, messages] of this.deliveryQueue.entries()) {
      if (messages.length === 0) continue

      // Take a batch of messages
      const batch = messages.slice(0, batchSize)
      const remaining = messages.slice(batchSize)

      // Update queue
      this.deliveryQueue.set(recipient, remaining)

      // Process batch
      await this.deliverBatch(recipient, batch)
    }
  }

  // Deliver batch of messages
  private async deliverBatch(recipient: string, messages: FederationMessage[]): Promise<void> {
    for (const message of messages) {
      try {
        await this.deliverMessage(message)
        message.delivered = true
        this.stats.messagesDelivered++
      } catch (error) {
        console.error(`Delivery failed for ${message.id} to ${recipient}:`, error)
        message.retries++

        if (message.retries < (this.options.maxRetries || 3)) {
          // Re-queue for retry
          if (!this.deliveryQueue.has(recipient)) {
            this.deliveryQueue.set(recipient, [])
          }
          this.deliveryQueue.get(recipient)!.push(message)
        } else {
          this.stats.deliveryFailures++
        }
      }
    }
  }

  // Deliver individual message
  private async deliverMessage(message: FederationMessage): Promise<void> {
    // In a real implementation, this would:
    // 1. Sign the HTTP Signature
    // 2. Send HTTP POST to recipient's inbox
    // 3. Handle authentication and rate limiting

    console.log(`Delivering message ${message.id} to ${message.actor}`)

    // Simulate delivery time
    await new Promise(resolve => setTimeout(resolve, Math.random() * 1000))
  }

  // Discover new instances
  private async discoverInstances(): Promise<void> {
    // In a real implementation, this would:
    // 1. Check known instance directories
    // 2. Parse WebFinger responses
    // 3. Analyze federation traffic patterns

    const knownInstances = [
      'mastodon.social',
      'mas.to',
      'social.uba.garden',
      'mathstodon.xyz',
      'hachyderm.net'
    ]

    for (const domain of knownInstances) {
      if (!this.instances.has(domain)) {
        try {
          await this.discoverInstance(domain)
        } catch (error) {
          console.warn(`Failed to discover ${domain}:`, error)
        }
      }
    }
  }

  // Update federation statistics
  private updateStatistics(): void {
    this.stats.totalUsers = Array.from(this.instances.values())
      .reduce((sum, instance) => sum + instance.users, 0)

    this.stats.totalPosts = Array.from(this.instances.values())
      .reduce((sum, instance) => sum + instance.statusCount, 0)

    // Update top instances
    const instanceStats = Array.from(this.instances.entries())
      .map(([domain, instance]) => ({
        domain,
        messages: this.stats.messagesDelivered,
        lastActivity: instance.lastActive
      }))
      .sort((a, b) => b.messages - a.messages)
      .slice(0, 10)

    this.stats.topInstances = instanceStats
  }

  // Perform health checks on known instances
  private async performHealthChecks(): Promise<void> {
    for (const [domain, instance] of this.instances) {
      try {
        // Check if instance is responding
        const response = await fetch(`https://${domain}/.well-known/nodeinfo`)
        const isHealthy = response.ok

        if (isHealthy && !this.blockedInstances.has(domain)) {
          instance.lastActive = new Date()
        } else if (!isHealthy && !this.blockedInstances.has(domain)) {
          // Instance appears to be down
          console.warn(`Instance ${domain} appears to be down`)
        }
      } catch (error) {
        console.warn(`Health check failed for ${domain}:`, error)
      }
    }
  }

  // Generate unique message ID
  private generateMessageId(): string {
    return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
  }

  // Get federation statistics
  getStatistics(): FederationStats {
    return { ...this.stats }
  }

  // Get instance information
  getInstance(domain: string): FederationInstance | undefined {
    return this.instances.get(domain)
  }

  // Get all instances
  getAllInstances(): FederationInstance[] {
    return Array.from(this.instances.values())
  }

  // Get blocked instances
  getBlockedInstances(): string[] {
    return Array.from(this.blockedInstances)
  }

  // Add content filter
  addContentFilter(filter: ContentFilter): void {
    this.contentFilters.set(filter.id, filter)
    console.log(`Added content filter: ${filter.name}`)
  }

  // Remove content filter
  removeContentFilter(filterId: string): void {
    this.contentFilters.delete(filterId)
    console.log(`Removed content filter: ${filterId}`)
  }

  // Subscribe to relay
  async subscribeToRelay(relayId: string, filters: string[] = []): Promise<RelaySubscription> {
    const subscription: RelaySubscription = {
      id: this.generateMessageId(),
      domain: relayId,
      subscribed: true,
      filters,
      priority: 1,
      createdAt: new Date(),
      lastSync: new Date()
    }

    this.relaySubscriptions.set(relayId, subscription)
    console.log(`Subscribed to relay: ${relayId}`)

    return subscription
  }

  // Unsubscribe from relay
  unsubscribeFromRelay(relayId: string): void {
    const subscription = this.relaySubscriptions.get(relayId)
    if (subscription) {
      subscription.subscribed = false
      console.log(`Unsubscribed from relay: ${relayId}`)
    }
  }

  // Global federation rules
  private globalFederationRules: FederationRule[] = [
    {
      id: 'spam-instances',
      name: 'Known Spam Instances',
      type: 'instance',
      action: 'block',
      conditions: {
        instances: [
          'spam.example.com',
          'fake.social.example.com'
        ]
      },
      actions: {
        blockDelivery: true,
        notifyModerators: true,
        addWarning: true
      }
    },
    {
      id: 'closed-instances',
      name: 'Closed Registration Instances',
      type: 'instance',
      action: 'filter',
      conditions: {
        instances: [] // Will be populated dynamically
      },
      actions: {
        blockDelivery: false,
        quarantineContent: true,
        notifyModerators: false
      }
    },
    {
      id: 'high-activity-instances',
      name: 'High Activity Filter',
      type: 'instance',
      action: 'filter',
      conditions: {
        users: { max: 1000000 }
      },
      actions: {
        rateLimit: 50,
        blockDelivery: false
      }
    }
  ]
}

// Usage example:
/*
const router = new ActivityPubFederationRouter('mastodon.example.com', {
  enableRelay: true,
  enableContentFiltering: true,
  enableRateLimiting: true,
  maxRetries: 3,
  batchSize: 10,
  relayEndpoints: ['relay.fediverse.network', 'relay.example.com']
})

// Discover some instances
await router.discoverInstance('mastodon.social')
await router.discoverInstance('mas.to')

// Add custom content filter
router.addContentFilter({
  id: 'custom-spam',
  name: 'Custom Spam Filter',
  type: 'regex',
  pattern: '\b(wow|amazing|incredible)\b',
  action: 'filter',
  enabled: true,
  severity: 3,
  falsePositiveRate: 0.1,
  lastUpdated: new Date()
})

// Process incoming message
const result = await router.processMessage({
  type: 'Create',
  actor: 'https://example.com/users/alice',
  object: {
    type: 'Note',
    content: 'Hello federated world!'
  },
  to: ['https://mastodon.social/users/bob'],
  cc: [],
  origin: 'example.com'
})

console.log('Processing result:', result)

// Get statistics
const stats = router.getStatistics()
console.log('Federation statistics:', stats)

// Subscribe to relay
await router.subscribeToRelay('relay.fediverse.network', ['public', 'unlisted'])
*/

export default ActivityPubFederationRouter