🎯 Рекомендуемые коллекции

Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать

Протокол ActivityPub Децентрализованной Социальной Сети

Примеры протокола ActivityPub для федеративных социальных сетей, совместимости с Mastodon и децентрализованных социальных медиа

💻 ActivityPub Hello World typescript

🟢 simple ⭐⭐⭐

Базовая реализация ActivityPub с акторами, объектами, активностями и основами федерации

⏱️ 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' | '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],
      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],
      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'],
      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],
      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

💻 Слой Совместимости Mastodon typescript

🟡 intermediate ⭐⭐⭐⭐

Создание совместимых с Mastodon экземпляров с полной поддержкой ActivityPub, лент и федерации

⏱️ 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

💻 Маршрутизатор Федерации и Ретрансляции typescript

🔴 complex ⭐⭐⭐⭐⭐

Продвинутый маршрутизатор федерации с сервисами ретрансляции, фильтрацией контента и межэкземплярной коммуникацией

⏱️ 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