Elysia Tools

Explora herramientas, ejemplos, explicaciones visuales y guías temáticas en un solo espacio claro.

Categorías populares

Temas

Idioma
ZHESPTRUFRDEEN

Elysia Tools

Navegación

Estándares web

Interacciones WebMentions entre Sitios

Ejemplos del protocolo WebMentions para interacciones entre sitios, sistemas de comentarios y redes sociales

Ejemplos

Entradas de esta colección

CódigojavascriptSimple

WebMentions Hello World

Implementación básica de WebMentions con envío y recepción de menciones entre sitios

Dificultad

4/10

Tiempo estimado

25 min

Etiquetas

webmentions, indieweb, social, interactions

Requisitos previos

JavaScript, Fetch API, HTML parsing, HTTP protocols

javascript
// WebMentions Hello World - Basic Cross-Site Interactions
// Simple implementation for sending and receiving webmentions

class WebMentionsHelloWorld {
  constructor(websiteUrl) {
    this.websiteUrl = websiteUrl
    this.mentions = [] // In-memory storage (use database in production)
    this.sentMentions = new Map() // Track sent mentions
  }

  // Discover WebMention endpoint for a URL
  async discoverWebMentionEndpoint(targetUrl) {
    try {
      // Fetch the target URL
      const response = await fetch(targetUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/html,application/xhtml+xml'
        }
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const html = await response.text()

      // Parse HTML to find WebMention endpoint
      const parser = new DOMParser()
      const doc = parser.parseFromString(html, 'text/html')

      // Check for <link> tags with rel="webmention"
      const linkTags = doc.querySelectorAll('link[rel~="webmention"]')
      if (linkTags.length > 0) {
        return linkTags[0].getAttribute('href')
      }

      // Check for <a> tags with rel="webmention"
      const anchorTags = doc.querySelectorAll('a[rel~="webmention"]')
      if (anchorTags.length > 0) {
        return anchorTags[0].getAttribute('href')
      }

      // Check HTTP Link header
      const linkHeader = response.headers.get('Link')
      if (linkHeader) {
        const links = this.parseLinkHeader(linkHeader)
        const webmentionLink = links.find(link => link.rel.includes('webmention'))
        if (webmentionLink) {
          return webmentionLink.href
        }
      }

      return null

    } catch (error) {
      console.error('Error discovering WebMention endpoint:', error)
      return null
    }
  }

  // Parse HTTP Link header
  parseLinkHeader(linkHeader) {
    const links = []
    const parts = linkHeader.split(',')

    for (const part of parts) {
      const match = part.match(/<([^>]+)>\s*;\s*rel="([^"]+)"/)
      if (match) {
        links.push({
          href: match[1],
          rel: match[2].split(/\s+/)
        })
      }
    }

    return links
  }

  // Send a WebMention
  async sendWebMention(sourceUrl, targetUrl) {
    try {
      console.log(`Sending WebMention from ${sourceUrl} to ${targetUrl}`)

      // Discover the WebMention endpoint
      const endpoint = await this.discoverWebMentionEndpoint(targetUrl)
      if (!endpoint) {
        throw new Error('No WebMention endpoint found for target URL')
      }

      // Make the WebMention request
      const formData = new FormData()
      formData.append('source', sourceUrl)
      formData.append('target', targetUrl)

      const response = await fetch(endpoint, {
        method: 'POST',
        body: formData,
        headers: {
          'Accept': 'application/json'
        }
      })

      const result = await response.json()

      if (response.ok && result.status === 'accepted') {
        // Store sent mention
        this.sentMentions.set(`${sourceUrl}:${targetUrl}`, {
          source: sourceUrl,
          target: targetUrl,
          endpoint: endpoint,
          status: 'accepted',
          sentAt: new Date(),
          response: result
        })

        console.log('WebMention sent successfully:', result)
        return {
          success: true,
          status: result.status,
          message: result.message || 'WebMention accepted'
        }
      } else {
        throw new Error(result.message || 'WebMention rejected')
      }

    } catch (error) {
      console.error('Error sending WebMention:', error)
      return {
        success: false,
        error: error.message
      }
    }
  }

  // Receive and verify a WebMention
  async receiveWebMention(sourceUrl, targetUrl) {
    try {
      console.log(`Processing WebMention from ${sourceUrl} to ${targetUrl}`)

      // Verify the source actually mentions the target
      const mentionsTarget = await this.verifyMention(sourceUrl, targetUrl)
      if (!mentionsTarget) {
        throw new Error('Source does not mention target URL')
      }

      // Extract content from source
      const content = await this.extractContent(sourceUrl)

      // Create mention object
      const mention = {
        id: this.generateMentionId(),
        source: sourceUrl,
        target: targetUrl,
        content: content,
        author: content.author || 'Anonymous',
        published: content.published || new Date().toISOString(),
        receivedAt: new Date(),
        verified: true,
        approved: this.shouldAutoApprove(content)
      }

      // Store the mention
      this.mentions.push(mention)

      console.log('WebMention received and processed:', mention)
      return {
        success: true,
        mention: mention
      }

    } catch (error) {
      console.error('Error processing WebMention:', error)
      return {
        success: false,
        error: error.message
      }
    }
  }

  // Verify that source URL actually mentions target URL
  async verifyMention(sourceUrl, targetUrl) {
    try {
      const response = await fetch(sourceUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/html,application/xhtml+xml'
        }
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const html = await response.text()
      const parser = new DOMParser()
      const doc = parser.parseFromString(html, 'text/html')

      // Check for links to target URL
      const links = doc.querySelectorAll(`a[href="${targetUrl}"], a[href="${targetUrl}/"]`)
      if (links.length > 0) {
        return true
      }

      // Check for text mentions of the URL
      const textContent = doc.body.textContent || ''
      return textContent.includes(targetUrl) || textContent.includes(targetUrl.replace(/^https?:\/\//, ''))

    } catch (error) {
      console.error('Error verifying mention:', error)
      return false
    }
  }

  // Extract structured content from source URL
  async extractContent(sourceUrl) {
    try {
      const response = await fetch(sourceUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/html,application/xhtml+xml'
        }
      })

      const html = await response.text()
      const parser = new DOMParser()
      const doc = parser.parseFromString(html, 'text/html')

      // Try to extract structured data (microformats2, schema.org, etc.)
      const content = {
        title: this.extractTitle(doc),
        author: this.extractAuthor(doc),
        published: this.extractPublished(doc),
        content: this.extractContentText(doc),
        url: sourceUrl
      }

      return content

    } catch (error) {
      console.error('Error extracting content:', error)
      return {
        title: 'Unknown',
        author: 'Anonymous',
        published: new Date().toISOString(),
        content: 'Failed to extract content',
        url: sourceUrl
      }
    }
  }

  extractTitle(doc) {
    // Try various title selectors
    const selectors = [
      'meta[property="og:title"]',
      'meta[name="twitter:title"]',
      '.p-name', // microformats2
      'title',
      'h1'
    ]

    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        const content = element.getAttribute('content') || element.textContent
        if (content) return content.trim()
      }
    }

    return 'Untitled'
  }

  extractAuthor(doc) {
    // Try various author selectors
    const selectors = [
      '.p-author .p-name', // microformats2
      'meta[property="article:author"]',
      'meta[name="author"]',
      '.author',
      '[rel="author"]'
    ]

    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        const content = element.getAttribute('content') || element.textContent
        if (content) return content.trim()
      }
    }

    return null
  }

  extractPublished(doc) {
    // Try various date selectors
    const selectors = [
      '.dt-published', // microformats2
      'meta[property="article:published_time"]',
      'meta[name="date"]',
      'time[datetime]'
    ]

    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        const content = element.getAttribute('content') || element.getAttribute('datetime') || element.textContent
        if (content) return content.trim()
      }
    }

    return new Date().toISOString()
  }

  extractContentText(doc) {
    // Try to find the main content
    const selectors = [
      '.e-content', // microformats2
      'meta[property="og:description"]',
      'meta[name="description"]',
      'article',
      'main'
    ]

    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        const content = element.getAttribute('content') || element.textContent
        if (content) {
          const text = content.trim()
          // Return first 200 characters
          return text.length > 200 ? text.substring(0, 200) + '...' : text
        }
      }
    }

    // Fallback to body text
    const bodyText = doc.body.textContent || ''
    return bodyText.trim().substring(0, 200) + '...'
  }

  // Auto-approval logic
  shouldAutoApprove(content) {
    // Simple auto-approval rules
    const suspiciousWords = ['spam', 'casino', 'viagra', 'lottery']
    const contentLower = content.content.toLowerCase()

    return !suspiciousWords.some(word => contentLower.includes(word))
  }

  // Generate unique mention ID
  generateMentionId() {
    return 'wm_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
  }

  // Get all mentions for a target URL
  getMentions(targetUrl, options = {}) {
    const {
      approved = true,
      limit = null,
      offset = 0
    } = options

    let mentions = this.mentions.filter(mention => mention.target === targetUrl)

    if (approved !== null) {
      mentions = mentions.filter(mention => mention.approved === approved)
    }

    // Sort by received date (newest first)
    mentions.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt))

    // Apply pagination
    if (limit) {
      mentions = mentions.slice(offset, offset + limit)
    }

    return mentions
  }

  // Get mentions statistics
  getMentionStats() {
    return {
      total: this.mentions.length,
      approved: this.mentions.filter(m => m.approved).length,
      pending: this.mentions.filter(m => !m.approved).length,
      sent: this.sentMentions.size,
      byTarget: this.groupMentionsByTarget()
    }
  }

  groupMentionsByTarget() {
    const grouped = {}
    for (const mention of this.mentions) {
      if (!grouped[mention.target]) {
        grouped[mention.target] = []
      }
      grouped[mention.target].push(mention)
    }
    return grouped
  }
}

// Example HTML structure with WebMention support
// <!DOCTYPE html>
// <html lang="en">
// <head>
//   <meta charset="UTF-8">
//   <meta name="viewport" content="width=device-width, initial-scale=1.0">
//   <title>My Blog Post</title>
//   <!-- WebMention endpoint declaration -->
//   <link rel="webmention" href="https://myblog.com/webmention">
//
//   <!-- Open Graph and other meta tags -->
//   <meta property="og:title" content="My Blog Post">
//   <meta property="og:description" content="An interesting blog post about WebMentions">
// </head>
// <body>
//   <article class="h-entry">
//     <header>
//       <h1 class="p-name">My Blog Post</h1>
//       <p class="p-author h-card">
//         By <span class="p-name">John Doe</span>
//       </p>
//       <time class="dt-published" datetime="2024-01-15T10:00:00Z">
//         January 15, 2024
//       </time>
//     </header>
//
//     <div class="e-content">
//       <p>This is an interesting blog post about WebMentions.
//       You can link to it from your own blog, and I'll receive a notification.</p>
//
//       <p>Check out this related post: <a href="https://example.com/related-post">Related Post</a></p>
//     </div>
//   </article>
//
//   <!-- WebMentions section -->
//   <section class="webmentions">
//     <h2>WebMentions</h2>
//     <div id="webmentions-list">
//       <!-- Mentions will be loaded here -->
//     </div>
//   </section>
//
//   <script src="webmentions.js"></script>
//   <script>
//     // Initialize WebMentions
//     const webmentions = new WebMentionsHelloWorld('https://myblog.com');
//
//     // Load mentions for current page
//     async function loadMentions() {
//       const currentUrl = window.location.href;
//       const mentions = webmentions.getMentions(currentUrl);
//
//       displayMentions(mentions);
//     }
//
//     function displayMentions(mentions) {
//       const container = document.getElementById('webmentions-list');
//
//       if (mentions.length === 0) {
//         container.innerHTML = '<p>No WebMentions yet.</p>';
//         return;
//       }
//
//       container.innerHTML = mentions.map(mention => `
//         <article class="webmention" data-id="${mention.id}">
//           <div class="webmention-header">
//             <strong>${mention.author}</strong>
//             <span class="webmention-date">
//               ${new Date(mention.receivedAt).toLocaleDateString()}
//             </span>
//           </div>
//           <div class="webmention-content">
//             <p>${mention.content}</p>
//           </div>
//           <div class="webmention-source">
//             <a href="${mention.source}" target="_blank" rel="noopener">
//               View original
//             </a>
//           </div>
//         </article>
//       `).join('');
//     }
//
//     // Send a WebMention (example)
//     async function sendWebMention(sourceUrl, targetUrl) {
//       const result = await webmentions.sendWebMention(sourceUrl, targetUrl);
//
//       if (result.success) {
//         alert('WebMention sent successfully!');
//       } else {
//         alert('Error sending WebMention: ' + result.error);
//       }
//     }
//
//     // Load mentions when page loads
//     document.addEventListener('DOMContentLoaded', loadMentions);
//   </script>
// </body>
// </html>

// Server-side WebMention endpoint example (Node.js/Express)
/*
const express = require('express');
const bodyParser = require('body-parser');
const { URL } = require('url');

const app = express();
app.use(bodyParser.urlencoded({ extended: true }));

// WebMention endpoint
app.post('/webmention', async (req, res) => {
  try {
    const { source, target } = req.body;

    if (!source || !target) {
      return res.status(400).json({
        error: 'Missing source or target URL'
      });
    }

    // Verify URLs
    try {
      new URL(source);
      new URL(target);
    } catch (e) {
      return res.status(400).json({
        error: 'Invalid source or target URL'
      });
    }

    // Process the WebMention
    const result = await processWebMention(source, target);

    if (result.success) {
      res.status(200).json({
        status: 'accepted',
        message: 'WebMention accepted'
      });
    } else {
      res.status(400).json({
        error: result.error
      });
    }

  } catch (error) {
    console.error('WebMention processing error:', error);
    res.status(500).json({
      error: 'Internal server error'
    });
  }
});

async function processWebMention(source, target) {
  // Verify that source mentions target
  const sourceContent = await fetch(source);
  const sourceText = await sourceContent.text();

  if (!sourceText.includes(target)) {
    return { success: false, error: 'Source does not mention target' };
  }

  // Store the WebMention (in a real app, save to database)
  console.log(`WebMention received: ${source} -> ${target}`);

  return { success: true };
}

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

// Usage example:
// const webmentions = new WebMentionsHelloWorld('https://myblog.com')
//
// // Send a WebMention
// webmentions.sendWebMention(
//   'https://myblog.com/reply-to-post',
//   'https://otherblog.com/original-post'
// )
//
// // Get mentions for a page
// const mentions = webmentions.getMentions('https://myblog.com/my-post')
// console.log('Mentions:', mentions)

export default WebMentionsHelloWorld
CódigotypescriptIntermedio

Sistema de Comentarios Avanzado

Sistema completo de comentarios usando WebMentions con moderación, filtrado de spam y contenido rico

Dificultad

8/10

Tiempo estimado

40 min

Etiquetas

webmentions, comments, social, moderation

Requisitos previos

TypeScript, HTML parsing, Content extraction, Spam detection

typescript
// Advanced WebMentions Comment System
// Rich content support, moderation, spam filtering, and engagement tracking

interface WebMention {
  id: string
  source: string
  target: string
  content: WebMentionContent
  author: WebMentionAuthor
  published: string
  receivedAt: Date
  verified: boolean
  approved: boolean
  spamScore: number
  moderationStatus: 'pending' | 'approved' | 'rejected' | 'spam'
  replies: string[]
  reactions: {
    likes: number
    reposts: number
    replies: number
  }
  metadata: {
    fetchDate?: Date
    lastChecked?: Date
    checkCount: number
  }
}

interface WebMentionContent {
  title: string
  text: string
  html?: string
  excerpt: string
  url: string
  published?: string
  type: 'post' | 'reply' | 'like' | 'repost' | 'mention'
  images?: string[]
  videos?: string[]
  language?: string
}

interface WebMentionAuthor {
  name: string
  url?: string
  photo?: string
  email?: string
  social?: {
    twitter?: string
    mastodon?: string
    github?: string
  }
}

interface ModerationRule {
  id: string
  name: string
  type: 'spam' | 'approval' | 'rejection'
  conditions: {
    keywords?: string[]
    minLength?: number
    maxLength?: number
    allowedDomains?: string[]
    blockedDomains?: string[]
    requireHttps?: boolean
    requireName?: boolean
  }
  actions: {
    autoApprove?: boolean
    autoReject?: boolean
    markSpam?: boolean
    requireModeration?: boolean
  }
}

class WebMentionsCommentSystem {
  private mentions: Map<string, WebMention> = new Map()
  private moderationRules: ModerationRule[] = []
  private spamFilters: SpamFilter[] = []
  private contentParsers: ContentParser[] = []

  constructor(
    private websiteUrl: string,
    private options: {
      autoApprove?: boolean
      enableSpamFilter?: boolean
      enableRichContent?: boolean
      moderationRules?: ModerationRule[]
    } = {}
  ) {
    this.setupDefaultModerationRules()
    this.setupDefaultSpamFilters()
    this.setupDefaultContentParsers()
  }

  // Process incoming WebMention
  async processWebMention(sourceUrl: string, targetUrl: string): Promise<{
    success: boolean
    mention?: WebMention
    error?: string
    status?: string
  }> {
    try {
      console.log(`Processing WebMention: ${sourceUrl} -> ${targetUrl}`)

      // Generate unique ID
      const mentionId = this.generateMentionId(sourceUrl, targetUrl)

      // Check if mention already exists
      if (this.mentions.has(mentionId)) {
        return {
          success: false,
          error: 'WebMention already processed',
          status: 'duplicate'
        }
      }

      // Verify the mention
      const verificationResult = await this.verifyWebMention(sourceUrl, targetUrl)
      if (!verificationResult.valid) {
        return {
          success: false,
          error: verificationResult.error || 'Invalid WebMention',
          status: 'invalid'
        }
      }

      // Extract content
      const content = await this.extractWebMentionContent(sourceUrl)

      // Parse content type
      const mentionType = this.detectMentionType(content, sourceUrl, targetUrl)

      // Extract author information
      const author = await this.extractAuthorInfo(sourceUrl)

      // Create mention object
      const mention: WebMention = {
        id: mentionId,
        source: sourceUrl,
        target: targetUrl,
        content: {
          ...content,
          type: mentionType
        },
        author,
        published: content.published || new Date().toISOString(),
        receivedAt: new Date(),
        verified: true,
        approved: false,
        spamScore: 0,
        moderationStatus: 'pending',
        replies: [],
        reactions: {
          likes: 0,
          reposts: 0,
          replies: 0
        },
        metadata: {
          fetchDate: new Date(),
          checkCount: 1
        }
      }

      // Apply spam filtering
      if (this.options.enableSpamFilter) {
        mention.spamScore = await this.calculateSpamScore(mention)
        mention.moderationStatus = this.determineModerationStatus(mention)
      }

      // Apply moderation rules
      this.applyModerationRules(mention)

      // Auto-approve if enabled and no spam detected
      if (this.options.autoApprove && mention.spamScore < 50 && mention.moderationStatus === 'pending') {
        mention.approved = true
        mention.moderationStatus = 'approved'
      }

      // Store mention
      this.mentions.set(mentionId, mention)

      console.log(`WebMention processed successfully: ${mentionId}`)
      return {
        success: true,
        mention,
        status: mention.moderationStatus
      }

    } catch (error) {
      console.error('Error processing WebMention:', error)
      return {
        success: false,
        error: error.message,
        status: 'error'
      }
    }
  }

  // Verify WebMention
  private async verifyWebMention(sourceUrl: string, targetUrl: string): Promise<{
    valid: boolean
    error?: string
  }> {
    try {
      // Validate URLs
      const source = new URL(sourceUrl)
      const target = new URL(targetUrl)

      // Check if target belongs to our website
      if (!target.origin.includes(new URL(this.websiteUrl).origin)) {
        return {
          valid: false,
          error: 'Target URL does not belong to this website'
        }
      }

      // Fetch source content
      const response = await fetch(sourceUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/html,application/xhtml+xml',
          'User-Agent': 'WebMentions-Comment-System/1.0'
        }
      })

      if (!response.ok) {
        return {
          valid: false,
          error: `HTTP ${response.status}: ${response.statusText}`
        }
      }

      const html = await response.text()

      // Verify that source mentions target
      const mentionsTarget = this.checkForMention(html, targetUrl)
      if (!mentionsTarget) {
        return {
          valid: false,
          error: 'Source does not mention target URL'
        }
      }

      // Additional verification checks
      const verificationResults = await Promise.all([
        this.checkSourceReputation(sourceUrl),
        this.validateContent(html),
        this.checkRateLimit(sourceUrl)
      ])

      const hasInvalidResult = verificationResults.some(result => !result.valid)
      if (hasInvalidResult) {
        const invalidResult = verificationResults.find(result => !result.valid)
        return {
          valid: false,
          error: invalidResult.error
        }
      }

      return { valid: true }

    } catch (error) {
      return {
        valid: false,
        error: error.message
      }
    }
  }

  // Check if source mentions target
  private checkForMention(html: string, targetUrl: string): boolean {
    const parser = new DOMParser()
    const doc = parser.parseFromString(html, 'text/html')

    // Check for direct links
    const links = doc.querySelectorAll(`a[href="${targetUrl}"], a[href="${targetUrl}/"]`)
    if (links.length > 0) return true

    // Check for URL in text content
    const textContent = doc.body.textContent || ''
    if (textContent.includes(targetUrl)) return true

    // Check for URL without protocol
    const targetWithoutProtocol = targetUrl.replace(/^https?:\/\//, '')
    if (textContent.includes(targetWithoutProtocol)) return true

    return false
  }

  // Extract content from source
  private async extractWebMentionContent(sourceUrl: string): Promise<WebMentionContent> {
    try {
      const response = await fetch(sourceUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/html,application/xhtml+xml'
        }
      })

      const html = await response.text()
      const parser = new DOMParser()
      const doc = parser.parseFromString(html, 'text/html')

      // Extract structured data using various parsers
      let content: WebMentionContent = {
        title: '',
        text: '',
        excerpt: '',
        url: sourceUrl,
        type: 'mention',
        language: doc.documentElement.getAttribute('lang') || 'en'
      }

      for (const parser of this.contentParsers) {
        try {
          const parsed = await parser.parse(doc, sourceUrl)
          if (parsed) {
            content = { ...content, ...parsed }
            break // Use first successful parser
          }
        } catch (error) {
          console.warn('Content parser failed:', error)
        }
      }

      // Fallback extraction
      if (!content.title || !content.text) {
        content.title = content.title || this.extractTitle(doc)
        content.text = content.text || this.extractText(doc)
        content.excerpt = this.createExcerpt(content.text)
      }

      return content

    } catch (error) {
      console.error('Error extracting content:', error)
      return {
        title: 'Unknown',
        text: 'Failed to extract content',
        excerpt: 'Failed to extract content',
        url: sourceUrl,
        type: 'mention'
      }
    }
  }

  // Extract author information
  private async extractAuthorInfo(sourceUrl: string): Promise<WebMentionAuthor> {
    try {
      const response = await fetch(sourceUrl)
      const html = await response.text()
      const parser = new DOMParser()
      const doc = parser.parseFromString(html, 'text/html')

      const author: WebMentionAuthor = {
        name: 'Anonymous'
      }

      // Try microformats2 first
      const authorCard = doc.querySelector('.h-card')
      if (authorCard) {
        author.name = this.getTextFromSelector(authorCard, '.p-name') || author.name
        author.url = this.getAttrFromSelector(authorCard, '.u-url') || author.url
        author.photo = this.getAttrFromSelector(authorCard, '.u-photo') || author.photo
        author.email = this.getTextFromSelector(authorCard, '.u-email') || author.email
      }

      // Fallback to other meta tags
      if (!author.name) {
        author.name = this.getMetaContent(doc, 'author') ||
                    this.getMetaContent(doc, 'article:author') ||
                    'Anonymous'
      }

      // Extract social profiles
      author.social = {
        twitter: this.extractSocialProfile(doc, 'twitter'),
        mastodon: this.extractSocialProfile(doc, 'mastodon'),
        github: this.extractSocialProfile(doc, 'github')
      }

      return author

    } catch (error) {
      console.error('Error extracting author:', error)
      return { name: 'Anonymous' }
    }
  }

  // Detect mention type
  private detectMentionType(content: WebMentionContent, sourceUrl: string, targetUrl: string): WebMentionContent['type'] {
    // Check for in-reply-to relationship
    if (content.text.toLowerCase().includes('reply') ||
        content.text.toLowerCase().includes('re:') ||
        this.isReplyTo(sourceUrl, targetUrl)) {
      return 'reply'
    }

    // Check for like/repost indicators
    if (content.text.toLowerCase().includes('like') ||
        content.text.toLowerCase().includes('❤️') ||
        content.text.toLowerCase().includes('👍')) {
      return 'like'
    }

    if (content.text.toLowerCase().includes('repost') ||
        content.text.toLowerCase().includes('🔄')) {
      return 'repost'
    }

    // Default to post/mention
    return content.text.length > 200 ? 'post' : 'mention'
  }

  // Calculate spam score
  private async calculateSpamScore(mention: WebMention): Promise<number> {
    let spamScore = 0

    for (const filter of this.spamFilters) {
      try {
        const score = await filter.analyze(mention)
        spamScore += score
      } catch (error) {
        console.warn('Spam filter failed:', error)
      }
    }

    return Math.min(100, Math.max(0, spamScore))
  }

  // Determine moderation status
  private determineModerationStatus(mention: WebMention): WebMention['moderationStatus'] {
    if (mention.spamScore >= 80) {
      return 'spam'
    } else if (mention.spamScore >= 60) {
      return 'rejected'
    } else if (mention.spamScore >= 30) {
      return 'pending'
    } else {
      return 'approved'
    }
  }

  // Apply moderation rules
  private applyModerationRules(mention: WebMention): void {
    for (const rule of this.moderationRules) {
      if (this.evaluateRule(rule, mention)) {
        // Apply rule actions
        if (rule.actions.autoApprove) {
          mention.approved = true
          mention.moderationStatus = 'approved'
        }
        if (rule.actions.autoReject) {
          mention.approved = false
          mention.moderationStatus = 'rejected'
        }
        if (rule.actions.markSpam) {
          mention.spamScore = 100
          mention.moderationStatus = 'spam'
        }
      }
    }
  }

  // Evaluate moderation rule
  private evaluateRule(rule: ModerationRule, mention: WebMention): boolean {
    const conditions = rule.conditions

    // Check keywords
    if (conditions.keywords) {
      const content = mention.content.text.toLowerCase()
      const hasKeywords = conditions.keywords.some(keyword =>
        content.includes(keyword.toLowerCase())
      )
      if (hasKeywords) return true
    }

    // Check content length
    if (conditions.minLength && mention.content.text.length < conditions.minLength) {
      return true
    }
    if (conditions.maxLength && mention.content.text.length > conditions.maxLength) {
      return true
    }

    // Check domain restrictions
    try {
      const sourceDomain = new URL(mention.source).hostname

      if (conditions.allowedDomains && !conditions.allowedDomains.includes(sourceDomain)) {
        return true
      }
      if (conditions.blockedDomains && conditions.blockedDomains.includes(sourceDomain)) {
        return true
      }

      if (conditions.requireHttps && !mention.source.startsWith('https://')) {
        return true
      }
    } catch (error) {
      return true // Invalid URL
    }

    // Check author requirements
    if (conditions.requireName && (!mention.author.name || mention.author.name === 'Anonymous')) {
      return true
    }

    return false
  }

  // Get mentions for target
  getMentions(targetUrl: string, options: {
    approved?: boolean
    type?: WebMentionContent['type']
    limit?: number
    offset?: number
    sortBy?: 'date' | 'relevance'
    sortOrder?: 'asc' | 'desc'
  } = {}): WebMention[] {
    let mentions = Array.from(this.mentions.values())
      .filter(mention => mention.target === targetUrl)

    // Apply filters
    if (options.approved !== undefined) {
      mentions = mentions.filter(mention => mention.approved === options.approved)
    }
    if (options.type) {
      mentions = mentions.filter(mention => mention.content.type === options.type)
    }

    // Sort
    const sortBy = options.sortBy || 'date'
    const sortOrder = options.sortOrder || 'desc'

    mentions.sort((a, b) => {
      let comparison = 0

      if (sortBy === 'date') {
        comparison = new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime()
      } else if (sortBy === 'relevance') {
        // Simple relevance scoring based on content type and reactions
        const scoreA = this.calculateRelevanceScore(a)
        const scoreB = this.calculateRelevanceScore(b)
        comparison = scoreA - scoreB
      }

      return sortOrder === 'desc' ? -comparison : comparison
    })

    // Apply pagination
    if (options.offset) {
      mentions = mentions.slice(options.offset)
    }
    if (options.limit) {
      mentions = mentions.slice(0, options.limit)
    }

    return mentions
  }

  // Get mention statistics
  getMentionStats(targetUrl: string) {
    const mentions = this.getMentions(targetUrl)
    const approved = this.getMentions(targetUrl, { approved: true })
    const pending = this.getMentions(targetUrl, { approved: false })

    const stats = {
      total: mentions.length,
      approved: approved.length,
      pending: pending.length,
      spam: mentions.filter(m => m.moderationStatus === 'spam').length,
      rejected: mentions.filter(m => m.moderationStatus === 'rejected').length,
      byType: this.groupByType(mentions),
      byDate: this.groupByDate(mentions),
      averageSpamScore: mentions.length > 0 ?
        mentions.reduce((sum, m) => sum + m.spamScore, 0) / mentions.length : 0
    }

    return stats
  }

  // Utility methods
  private generateMentionId(source: string, target: string): string {
    return btoa(`${source}|${target}`).replace(/[+/=]/g, '').substring(0, 16)
  }

  private createExcerpt(text: string, length: number = 200): string {
    const cleaned = text.replace(/<[^>]*>/g, '').trim()
    return cleaned.length > length ? cleaned.substring(0, length) + '...' : cleaned
  }

  private extractTitle(doc: Document): string {
    const selectors = ['meta[property="og:title"]', 'title', 'h1']
    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        const content = element.getAttribute('content') || element.textContent
        if (content) return content.trim()
      }
    }
    return 'Untitled'
  }

  private extractText(doc: Document): string {
    const selectors = [
      '.e-content',
      'article',
      'main',
      '[role="main"]',
      'body'
    ]
    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        return element.textContent || ''
      }
    }
    return ''
  }

  private getTextFromSelector(element: Element, selector: string): string | null {
    const el = element.querySelector(selector)
    return el ? el.textContent?.trim() || null : null
  }

  private getAttrFromSelector(element: Element, selector: string): string | null {
    const el = element.querySelector(selector)
    return el ? el.getAttribute('href') || el.getAttribute('src') : null
  }

  private getMetaContent(doc: Document, name: string): string | null {
    const selector = `meta[name="${name}"], meta[property="${name}"]`
    const element = doc.querySelector(selector)
    return element ? element.getAttribute('content') : null
  }

  private extractSocialProfile(doc: Document, platform: string): string | null {
    const selectors = [
      `a[href*="${platform}.com"]`,
      `a[href*="${platform}.org"]`,
      `[rel="me"][href*="${platform}"]`
    ]
    for (const selector of selectors) {
      const element = doc.querySelector(selector)
      if (element) {
        return element.getAttribute('href')
      }
    }
    return null
  }

  private calculateRelevanceScore(mention: WebMention): number {
    let score = 0

    // Content type scoring
    const typeScores = { reply: 10, post: 8, like: 3, repost: 5, mention: 2 }
    score += typeScores[mention.content.type] || 0

    // Reaction scoring
    score += mention.reactions.likes * 2
    score += mention.reactions.reposts * 3
    score += mention.reactions.replies * 4

    // Content quality scoring
    if (mention.content.text.length > 100) score += 2
    if (mention.author.name !== 'Anonymous') score += 1
    if (mention.author.photo) score += 1

    return score
  }

  private groupByType(mentions: WebMention[]): Record<string, number> {
    return mentions.reduce((groups, mention) => {
      const type = mention.content.type
      groups[type] = (groups[type] || 0) + 1
      return groups
    }, {} as Record<string, number>)
  }

  private groupByDate(mentions: WebMention[]): Record<string, number> {
    return mentions.reduce((groups, mention) => {
      const date = new Date(mention.receivedAt).toISOString().split('T')[0]
      groups[date] = (groups[date] || 0) + 1
      return groups
    }, {} as Record<string, number>)
  }

  // Setup default configurations
  private setupDefaultModerationRules(): void {
    this.moderationRules = [
      {
        id: 'short-content',
        name: 'Reject very short content',
        type: 'rejection',
        conditions: {
          minLength: 10
        },
        actions: {
          autoReject: true
        }
      },
      {
        id: 'spam-keywords',
        name: 'Block spam keywords',
        type: 'spam',
        conditions: {
          keywords: ['viagra', 'casino', 'lottery', 'spam', 'buy now']
        },
        actions: {
          markSpam: true
        }
      },
      {
        id: 'require-https',
        name: 'Require HTTPS sources',
        type: 'rejection',
        conditions: {
          requireHttps: true
        },
        actions: {
          requireModeration: true
        }
      }
    ]
  }

  private setupDefaultSpamFilters(): void {
    this.spamFilters = [
      new KeywordSpamFilter(),
      new LinkSpamFilter(),
      new RepeatedContentFilter(),
      new LanguageFilter()
    ]
  }

  private setupDefaultContentParsers(): void {
    this.contentParsers = [
      new MicroformatsParser(),
      new OpenGraphParser(),
      new JSONLDParser(),
      new TwitterCardParser()
    ]
  }

  // Additional verification methods
  private async checkSourceReputation(sourceUrl: string): Promise<{ valid: boolean; error?: string }> {
    // Implement source reputation checking
    return { valid: true }
  }

  private async validateContent(html: string): Promise<{ valid: boolean; error?: string }> {
    // Implement content validation
    return { valid: true }
  }

  private async checkRateLimit(sourceUrl: string): Promise<{ valid: boolean; error?: string }> {
    // Implement rate limiting
    return { valid: true }
  }

  private isReplyTo(sourceUrl: string, targetUrl: string): boolean {
    // Check if source is a reply to target
    return sourceUrl.includes('/reply/') || sourceUrl.includes('?in-reply-to=')
  }
}

// Supporting classes
class SpamFilter {
  async analyze(mention: WebMention): Promise<number> {
    return 0
  }
}

class KeywordSpamFilter extends SpamFilter {
  private spamKeywords = [
    'click here', 'buy now', 'free money', 'make money fast',
    'viagra', 'casino', 'lottery', 'winner', 'congratulations'
  ]

  async analyze(mention: WebMention): Promise<number> {
    const text = mention.content.text.toLowerCase()
    let score = 0

    for (const keyword of this.spamKeywords) {
      if (text.includes(keyword)) {
        score += 20
      }
    }

    return score
  }
}

class LinkSpamFilter extends SpamFilter {
  async analyze(mention: WebMention): Promise<number> {
    const linkRegex = /https?:\/\/[^\s]+/g
    const links = mention.content.text.match(linkRegex) || []

    if (links.length > 5) return 30
    if (links.length > 3) return 15

    return 0
  }
}

class RepeatedContentFilter extends SpamFilter {
  async analyze(mention: WebMention): Promise<number> {
    // Check for repeated characters or suspicious patterns
    const repeatedChars = /(.)\1{4,}/.test(mention.content.text)
    if (repeatedChars) return 25

    const excessiveCaps = (mention.content.text.match(/[A-Z]/g) || []).length / mention.content.text.length
    if (excessiveCaps > 0.5) return 20

    return 0
  }
}

class LanguageFilter extends SpamFilter {
  async analyze(mention: WebMention): Promise<number> {
    // Check for language mismatches or suspicious text
    const suspiciousWords = ['asdf', 'qwerty', 'test', 'demo']
    const text = mention.content.text.toLowerCase()

    for (const word of suspiciousWords) {
      if (text.includes(word)) {
        return 15
      }
    }

    return 0
  }
}

class ContentParser {
  async parse(doc: Document, sourceUrl: string): Promise<Partial<WebMentionContent>> {
    return {}
  }
}

class MicroformatsParser extends ContentParser {
  async parse(doc: Document, sourceUrl: string): Promise<Partial<WebMentionContent>> {
    const hEntry = doc.querySelector('.h-entry')
    if (!hEntry) return {}

    const content: Partial<WebMentionContent> = {
      title: this.getText(hEntry, '.p-name'),
      text: this.getText(hEntry, '.e-content'),
      url: this.getAttr(hEntry, '.u-url') || sourceUrl
    }

    const published = this.getAttr(hEntry, '.dt-published')
    if (published) {
      content.published = published
    }

    return content
  }

  private getText(element: Element, selector: string): string {
    const el = element.querySelector(selector)
    return el ? el.textContent?.trim() || '' : ''
  }

  private getAttr(element: Element, selector: string): string {
    const el = element.querySelector(selector)
    return el ? (el.getAttribute('href') || el.getAttribute('datetime') || '') : ''
  }
}

class OpenGraphParser extends ContentParser {
  async parse(doc: Document): Promise<Partial<WebMentionContent>> {
    const content: Partial<WebMentionContent> = {}

    const title = doc.querySelector('meta[property="og:title"]')
    if (title) {
      content.title = title.getAttribute('content') || ''
    }

    const description = doc.querySelector('meta[property="og:description"]')
    if (description) {
      content.text = description.getAttribute('content') || ''
    }

    return content
  }
}

class JSONLDParser extends ContentParser {
  async parse(doc: Document): Promise<Partial<WebMentionContent>> {
    const scripts = doc.querySelectorAll('script[type="application/ld+json"]')

    for (const script of scripts) {
      try {
        const data = JSON.parse(script.textContent || '')
        if (data['@type'] === 'BlogPosting' || data['@type'] === 'Article') {
          return {
            title: data.headline || '',
            text: data.description || data.text || '',
            published: data.datePublished || ''
          }
        }
      } catch (error) {
        continue
      }
    }

    return {}
  }
}

class TwitterCardParser extends ContentParser {
  async parse(doc: Document): Promise<Partial<WebMentionContent>> {
    const content: Partial<WebMentionContent> = {}

    const title = doc.querySelector('meta[name="twitter:title"]')
    if (title) {
      content.title = title.getAttribute('content') || ''
    }

    const description = doc.querySelector('meta[name="twitter:description"]')
    if (description) {
      content.text = description.getAttribute('content') || ''
    }

    return content
  }
}

export default WebMentionsCommentSystem
CódigotypescriptComplejo

Agregador de Redes Sociales

Agregar contenido de múltiples plataformas sociales usando WebMentions y ActivityPub

Dificultad

9/10

Tiempo estimado

50 min

Etiquetas

webmentions, social media, aggregator, activitypub

Requisitos previos

Advanced TypeScript, Social media APIs, ActivityPub protocol, Data aggregation

typescript
// Social Network Aggregator with WebMentions and ActivityPub
// Aggregate content from Twitter, Mastodon, and other federated platforms

interface SocialPost {
  id: string
  platform: 'twitter' | 'mastodon' | 'bluesky' | 'web' | 'activitypub'
  url: string
  content: string
  author: SocialAuthor
  publishedAt: Date
  likes?: number
  shares?: number
  replies?: number
  hashtags?: string[]
  mentions?: string[]
  media?: SocialMedia[]
  replyTo?: string
  thread?: SocialPost[]
  webmentions?: WebMention[]
}

interface SocialAuthor {
  id: string
  name: string
  username: string
  avatar?: string
  url?: string
  verified?: boolean
  followers?: number
  bio?: string
  location?: string
  website?: string
  social?: {
    twitter?: string
    mastodon?: string
    bluesky?: string
    github?: string
  }
}

interface SocialMedia {
  type: 'image' | 'video' | 'audio' | 'link'
  url: string
  thumbnail?: string
  description?: string
  width?: number
  height?: number
  duration?: number
}

interface WebMention {
  id: string
  source: string
  target: string
  content: string
  author: SocialAuthor
  publishedAt: Date
  type: 'like' | 'repost' | 'reply' | 'mention'
}

class SocialNetworkAggregator {
  private posts: Map<string, SocialPost> = new Map()
  private authors: Map<string, SocialAuthor> = new Map()
  private webmentionProcessor: WebMentionsCommentSystem
  private activityPubClient: ActivityPubClient

  constructor(
    private config: {
      websiteUrl: string
      platforms: SocialPlatform[]
      enableWebmentions: boolean
      enableActivityPub: boolean
      aggregationRules: AggregationRule[]
    }
  ) {
    this.webmentionProcessor = new WebMentionsCommentSystem(this.config.websiteUrl)
    this.activityPubClient = new ActivityPubClient(this.config.websiteUrl)
  }

  // Aggregate content from all configured platforms
  async aggregateContent(targetUrl: string): Promise<{
    posts: SocialPost[]
    authors: SocialAuthor[]
    statistics: AggregationStats
  }> {
    console.log(`Aggregating content for: ${targetUrl}`)

    const allPosts: SocialPost[] = []
    const allAuthors: Map<string, SocialAuthor> = new Map()

    // Aggregate from each platform
    for (const platform of this.config.platforms) {
      try {
        const posts = await this.aggregateFromPlatform(platform, targetUrl)
        allPosts.push(...posts)

        // Collect authors
        posts.forEach(post => {
          if (!allAuthors.has(post.author.id)) {
            allAuthors.set(post.author.id, post.author)
          }
        })

      } catch (error) {
        console.error(`Error aggregating from ${platform.name}:`, error)
      }
    }

    // Process WebMentions
    if (this.config.enableWebmentions) {
      const webmentionPosts = await this.aggregateWebmentions(targetUrl)
      allPosts.push(...webmentionPosts)
    }

    // Process ActivityPub interactions
    if (this.config.enableActivityPub) {
      const activityPubPosts = await this.aggregateActivityPub(targetUrl)
      allPosts.push(...activityPubPosts)
    }

    // Apply aggregation rules
    const filteredPosts = this.applyAggregationRules(allPosts)

    // Sort and rank posts
    const rankedPosts = this.rankPosts(filteredPosts)

    // Calculate statistics
    const statistics = this.calculateStatistics(rankedPosts)

    return {
      posts: rankedPosts,
      authors: Array.from(allAuthors.values()),
      statistics
    }
  }

  // Aggregate from specific platform
  private async aggregateFromPlatform(platform: SocialPlatform, targetUrl: string): Promise<SocialPost[]> {
    switch (platform.type) {
      case 'twitter':
        return this.aggregateTwitter(platform, targetUrl)
      case 'mastodon':
        return this.aggregateMastodon(platform, targetUrl)
      case 'bluesky':
        return this.aggregateBluesky(platform, targetUrl)
      case 'activitypub':
        return this.aggregateActivityPubInstance(platform, targetUrl)
      default:
        return []
    }
  }

  // Aggregate Twitter content
  private async aggregateTwitter(platform: SocialPlatform, targetUrl: string): Promise<SocialPost[]> {
    console.log('Aggregating from Twitter...')

    // Search for tweets mentioning the target URL
    const searchQuery = this.buildSearchQuery(targetUrl)
    const tweets = await this.searchTwitterTweets(searchQuery, platform.apiKey)

    return tweets.map(tweet => this.convertTwitterToSocialPost(tweet))
  }

  // Aggregate Mastodon content
  private async aggregateMastodon(platform: SocialPlatform, targetUrl: string): Promise<SocialPost[]> {
    console.log('Aggregating from Mastodon...')

    const posts: SocialPost[] = []

    // Search across multiple Mastodon instances
    for (const instance of platform.instances || []) {
      try {
        const searchResults = await this.searchMastodonInstance(instance, targetUrl)
        posts.push(...searchResults.map(post => this.convertMastodonToSocialPost(post, instance)))
      } catch (error) {
        console.warn(`Failed to search Mastodon instance ${instance}:`, error)
      }
    }

    return posts
  }

  // Aggregate Bluesky content
  private async aggregateBluesky(platform: SocialPlatform, targetUrl: string): Promise<SocialPost[]> {
    console.log('Aggregating from Bluesky...')

    const searchResults = await this.searchBluesky(targetUrl, platform.apiKey)
    return searchResults.map(post => this.convertBlueskyToSocialPost(post))
  }

  // Aggregate WebMentions
  private async aggregateWebmentions(targetUrl: string): Promise<SocialPost[]> {
    console.log('Aggregating WebMentions...')

    const mentions = this.webmentionProcessor.getMentions(targetUrl, { approved: true })

    return mentions.map(mention => ({
      id: mention.id,
      platform: 'web' as const,
      url: mention.source,
      content: mention.content.text,
      author: this.convertWebmentionAuthor(mention.author),
      publishedAt: new Date(mention.published),
      type: 'mention',
      webmentions: [mention]
    }))
  }

  // Aggregate ActivityPub content
  private async aggregateActivityPub(targetUrl: string): Promise<SocialPost[]> {
    console.log('Aggregating from ActivityPub...')

    const activities = await this.activityPubClient.searchInteractions(targetUrl)
    return activities.map(activity => this.convertActivityPubToSocialPost(activity))
  }

  // Search Twitter for tweets
  private async searchTwitterTweets(query: string, apiKey: string): Promise<TwitterTweet[]> {
    try {
      const response = await fetch(`https://api.twitter.com/2/tweets/search/recent?query=${encodeURIComponent(query)}&expansions=author_id,attachments.media&tweet.fields=public_metrics,created_at&user.fields=username,name,profile_image_url,verified,public_metrics`, {
        headers: {
          'Authorization': `Bearer ${apiKey}`
        }
      })

      if (!response.ok) {
        throw new Error(`Twitter API error: ${response.statusText}`)
      }

      const data = await response.json()
      return data.data || []

    } catch (error) {
      console.error('Twitter search error:', error)
      return []
    }
  }

  // Search Mastodon instance
  private async searchMastodonInstance(instance: string, targetUrl: string): Promise<MastodonPost[]> {
    try {
      const response = await fetch(`https://${instance}/api/v2/search?q=${encodeURIComponent(targetUrl)}&resolve=true&type=statuses`, {
        headers: {
          'Accept': 'application/json'
        }
      })

      if (!response.ok) {
        throw new Error(`Mastodon API error: ${response.statusText}`)
      }

      const data = await response.json()
      return data.statuses || []

    } catch (error) {
      console.error(`Mastodon search error for ${instance}:`, error)
      return []
    }
  }

  // Search Bluesky
  private async searchBluesky(query: string, apiKey: string): Promise<BlueskyPost[]> {
    try {
      const response = await fetch(`https://api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=${encodeURIComponent(query)}`, {
        headers: {
          'Authorization': `Bearer ${apiKey}`
        }
      })

      if (!response.ok) {
        throw new Error(`Bluesky API error: ${response.statusText}`)
      }

      const data = await response.json()
      return data.posts || []

    } catch (error) {
      console.error('Bluesky search error:', error)
      return []
    }
  }

  // Build search query for platform
  private buildSearchQuery(targetUrl: string): string {
    // Extract domain for better searching
    const url = new URL(targetUrl)
    const domain = url.hostname

    // Build comprehensive search query
    const queryTerms = [
      `\"${targetUrl.replace(/^https?:\/\//, '')}\"`, // Exact URL
      domain, // Domain name
      url.pathname, // Path
      'lang:en' // English results only
    ]

    // Add negative terms to filter out noise
    const negativeTerms = ['-filter:replies', '-filter:retweets']

    return [...queryTerms, ...negativeTerms].join(' ')
  }

  // Convert platform-specific formats to SocialPost
  private convertTwitterToSocialPost(tweet: TwitterTweet): SocialPost {
    return {
      id: tweet.id,
      platform: 'twitter',
      url: `https://twitter.com/i/web/status/${tweet.id}`,
      content: tweet.text,
      author: {
        id: tweet.author_id,
        name: tweet.author.name,
        username: tweet.author.username,
        avatar: tweet.author.profile_image_url,
        url: `https://twitter.com/${tweet.author.username}`,
        verified: tweet.author.verified,
        followers: tweet.author.public_metrics?.followers_count
      },
      publishedAt: new Date(tweet.created_at),
      likes: tweet.public_metrics?.like_count,
      shares: tweet.public_metrics?.retweet_count,
      replies: tweet.public_metrics?.reply_count,
      hashtags: this.extractHashtags(tweet.text),
      mentions: this.extractMentions(tweet.text),
      media: this.convertTwitterMedia(tweet.attachments)
    }
  }

  private convertMastodonToSocialPost(post: MastodonPost, instance: string): SocialPost {
    return {
      id: post.id,
      platform: 'mastodon',
      url: post.url,
      content: post.content,
      author: {
        id: post.account.id,
        name: post.account.display_name,
        username: post.account.username,
        avatar: post.account.avatar,
        url: post.account.url,
        followers: post.account.followers_count,
        bio: post.account.note
      },
      publishedAt: new Date(post.created_at),
      likes: post.favourites_count,
      shares: post.reblogs_count,
      replies: post.replies_count,
      hashtags: post.tags?.map(tag => tag.name),
      mentions: post.mentions?.map(mention => mention.acct),
      media: this.convertMastodonMedia(post.media_attachments),
      replyTo: post.in_reply_to_id
    }
  }

  private convertBlueskyToSocialPost(post: BlueskyPost): SocialPost {
    return {
      id: post.uri,
      platform: 'bluesky',
      url: `https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('/').pop()}`,
      content: post.record.text,
      author: {
        id: post.author.did,
        name: post.author.displayName,
        username: post.author.handle,
        avatar: post.author.avatar,
        url: `https://bsky.app/profile/${post.author.handle}`,
        followers: post.author.followersCount
      },
      publishedAt: new Date(post.record.createdAt),
      likes: post.likeCount,
      replies: post.replyCount,
      reposts: post.repostCount,
      hashtags: this.extractBlueskyTags(post.record.text),
      media: this.convertBlueskyMedia(post.embed)
    }
  }

  private convertWebmentionAuthor(author: any): SocialAuthor {
    return {
      id: author.url || 'unknown',
      name: author.name || 'Anonymous',
      username: author.name?.toLowerCase().replace(/\s+/g, '') || 'anonymous',
      avatar: author.photo,
      url: author.url,
      social: author.social
    }
  }

  private convertActivityPubToSocialPost(activity: ActivityPubActivity): SocialPost {
    const object = activity.object as ActivityPubObject

    return {
      id: activity.id,
      platform: 'activitypub',
      url: object.url,
      content: object.content,
      author: {
        id: object.attributedTo.id,
        name: object.attributedTo.name,
        username: object.attributedTo.preferredUsername,
        avatar: object.attributedTo.icon?.url,
        url: object.attributedTo.url,
        followers: object.attributedTo.followersCount
      },
      publishedAt: new Date(object.published),
      likes: object.likeCount || 0,
      shares: object.shareCount || 0,
      replies: object.replyCount || 0,
      hashtags: object.tag?.filter(tag => tag.type === 'Hashtag').map(tag => tag.name),
      mentions: object.tag?.filter(tag => tag.type === 'Mention').map(tag => tag.name)
    }
  }

  // Media conversion methods
  private convertTwitterMedia(attachments: TwitterMedia[]): SocialMedia[] {
    if (!attachments) return []

    return attachments.map(media => ({
      type: media.type === 'photo' ? 'image' : 'video',
      url: media.url,
      description: media.alt_text,
      width: media.width,
      height: media.height
    }))
  }

  private convertMastodonMedia(attachments: MastodonMedia[]): SocialMedia[] {
    if (!attachments) return []

    return attachments.map(media => ({
      type: media.type,
      url: media.url,
      thumbnail: media.preview_url,
      description: media.description,
      width: media.meta?.original?.width,
      height: media.meta?.original?.height,
      duration: media.meta?.duration
    }))
  }

  private convertBlueskyMedia(embed: any): SocialMedia[] {
    if (!embed) return []

    if (embed.$type === 'app.bsky.embed.images.view') {
      return embed.images.map((img: any) => ({
        type: 'image',
        url: img.fullsize,
        thumbnail: img.thumb,
        description: img.alt,
        width: img.aspectRatio?.width,
        height: img.aspectRatio?.height
      }))
    }

    return []
  }

  // Text processing utilities
  private extractHashtags(text: string): string[] {
    const hashtags = text.match(/#[\w]+/g)
    return hashtags ? hashtags.map(tag => tag.substring(1)) : []
  }

  private extractMentions(text: string): string[] {
    const mentions = text.match(/@[\w]+/g)
    return mentions ? mentions.map(mention => mention.substring(1)) : []
  }

  private extractBlueskyTags(text: string): string[] {
    // Bluesky doesn't use hashtags in the same way
    return []
  }

  // Apply aggregation rules
  private applyAggregationRules(posts: SocialPost[]): SocialPost[] {
    let filteredPosts = [...posts]

    for (const rule of this.config.aggregationRules) {
      filteredPosts = this.applyRule(filteredPosts, rule)
    }

    // Remove duplicates
    filteredPosts = this.deduplicatePosts(filteredPosts)

    return filteredPosts
  }

  private applyRule(posts: SocialPost[], rule: AggregationRule): SocialPost[] {
    return posts.filter(post => {
      // Apply rule logic
      if (rule.minFollowers && post.author.followers && post.author.followers < rule.minFollowers) {
        return false
      }

      if (rule.blockedWords) {
        const text = post.content.toLowerCase()
        if (rule.blockedWords.some(word => text.includes(word.toLowerCase()))) {
          return false
        }
      }

      if (rule.requireVerified && !post.author.verified) {
        return false
      }

      if (rule.maxAge) {
        const age = Date.now() - post.publishedAt.getTime()
        if (age > rule.maxAge) {
          return false
        }
      }

      return true
    })
  }

  // Remove duplicate posts
  private deduplicatePosts(posts: SocialPost[]): SocialPost[] {
    const seen = new Set<string>()
    return posts.filter(post => {
      // Create a unique key based on content and author
      const key = `${post.author.username}:${post.content.substring(0, 100)}`
      if (seen.has(key)) {
        return false
      }
      seen.add(key)
      return true
    })
  }

  // Rank posts by relevance and engagement
  private rankPosts(posts: SocialPost[]): SocialPost[] {
    return posts.sort((a, b) => {
      const scoreA = this.calculatePostScore(a)
      const scoreB = this.calculatePostScore(b)
      return scoreB - scoreA
    })
  }

  private calculatePostScore(post: SocialPost): number {
    let score = 0

    // Base score for content
    score += post.content.length > 100 ? 10 : 5

    // Engagement metrics
    score += (post.likes || 0) * 2
    score += (post.shares || 0) * 3
    score += (post.replies || 0) * 4

    // Author credibility
    if (post.author.verified) score += 20
    if (post.author.followers && post.author.followers > 1000) score += 10

    // Recency (more recent posts get higher scores)
    const ageHours = (Date.now() - post.publishedAt.getTime()) / (1000 * 60 * 60)
    score += Math.max(0, 20 - ageHours)

    // Platform weight
    const platformWeights = { twitter: 1.2, mastodon: 1.0, bluesky: 1.1, web: 0.8, activitypub: 1.0 }
    score *= platformWeights[post.platform] || 1.0

    return score
  }

  // Calculate aggregation statistics
  private calculateStatistics(posts: SocialPost[]): AggregationStats {
    const stats: AggregationStats = {
      totalPosts: posts.length,
      byPlatform: this.groupByPlatform(posts),
      byDate: this.groupByDate(posts),
      topAuthors: this.getTopAuthors(posts),
      engagementMetrics: this.calculateEngagementMetrics(posts),
      hashtags: this.getTopHashtags(posts),
      timeRange: this.getTimeRange(posts)
    }

    return stats
  }

  private groupByPlatform(posts: SocialPost[]): Record<string, number> {
    return posts.reduce((groups, post) => {
      groups[post.platform] = (groups[post.platform] || 0) + 1
      return groups
    }, {} as Record<string, number>)
  }

  private groupByDate(posts: SocialPost[]): Record<string, number> {
    return posts.reduce((groups, post) => {
      const date = post.publishedAt.toISOString().split('T')[0]
      groups[date] = (groups[date] || 0) + 1
      return groups
    }, {} as Record<string, number>)
  }

  private getTopAuthors(posts: SocialPost[]): Array<{ author: SocialAuthor; postCount: number }> {
    const authorCounts = posts.reduce((counts, post) => {
      counts[post.author.id] = {
        author: post.author,
        postCount: (counts[post.author.id]?.postCount || 0) + 1
      }
      return counts
    }, {} as Record<string, { author: SocialAuthor; postCount: number }>)

    return Object.values(authorCounts)
      .sort((a, b) => b.postCount - a.postCount)
      .slice(0, 10)
  }

  private calculateEngagementMetrics(posts: SocialPost[]) {
    const totals = posts.reduce((metrics, post) => {
      metrics.totalLikes += post.likes || 0
      metrics.totalShares += post.shares || 0
      metrics.totalReplies += post.replies || 0
      return metrics
    }, { totalLikes: 0, totalShares: 0, totalReplies: 0 })

    return {
      ...totals,
      averageLikes: totals.totalLikes / posts.length,
      averageShares: totals.totalShares / posts.length,
      averageReplies: totals.totalReplies / posts.length
    }
  }

  private getTopHashtags(posts: SocialPost[]): Array<{ hashtag: string; count: number }> {
    const hashtagCounts = posts.reduce((counts, post) => {
      if (post.hashtags) {
        post.hashtags.forEach(hashtag => {
          counts[hashtag] = (counts[hashtag] || 0) + 1
        })
      }
      return counts
    }, {} as Record<string, number>)

    return Object.entries(hashtagCounts)
      .map(([hashtag, count]) => ({ hashtag, count }))
      .sort((a, b) => b.count - a.count)
      .slice(0, 20)
  }

  private getTimeRange(posts: SocialPost[]): { earliest: Date; latest: Date } {
    if (posts.length === 0) {
      return {
        earliest: new Date(),
        latest: new Date()
      }
    }

    const dates = posts.map(post => post.publishedAt)
    return {
      earliest: new Date(Math.min(...dates.map(d => d.getTime()))),
      latest: new Date(Math.max(...dates.map(d => d.getTime())))
    }
  }
}

// Supporting interfaces and types
interface SocialPlatform {
  type: 'twitter' | 'mastodon' | 'bluesky' | 'activitypub'
  name: string
  apiKey?: string
  instances?: string[] // For Mastodon
}

interface AggregationRule {
  name: string
  minFollowers?: number
  blockedWords?: string[]
  requireVerified?: boolean
  maxAge?: number // milliseconds
}

interface AggregationStats {
  totalPosts: number
  byPlatform: Record<string, number>
  byDate: Record<string, number>
  topAuthors: Array<{ author: SocialAuthor; postCount: number }>
  engagementMetrics: {
    totalLikes: number
    totalShares: number
    totalReplies: number
    averageLikes: number
    averageShares: number
    averageReplies: number
  }
  hashtags: Array<{ hashtag: string; count: number }>
  timeRange: { earliest: Date; latest: Date }
}

// Placeholder interfaces for platform-specific data
interface TwitterTweet {
  id: string
  text: string
  author_id: string
  created_at: string
  public_metrics?: {
    like_count: number
    retweet_count: number
    reply_count: number
  }
  author: {
    id: string
    name: string
    username: string
    profile_image_url: string
    verified: boolean
    public_metrics?: {
      followers_count: number
    }
  }
  attachments?: TwitterMedia[]
}

interface TwitterMedia {
  media_key: string
  type: 'photo' | 'video'
  url: string
  preview_image_url?: string
  width?: number
  height?: number
  alt_text?: string
}

interface MastodonPost {
  id: string
  url: string
  content: string
  created_at: string
  account: {
    id: string
    display_name: string
    username: string
    avatar: string
    url: string
    followers_count: number
    note: string
  }
  favourites_count: number
  reblogs_count: number
  replies_count: number
  tags?: Array<{ name: string }>
  mentions?: Array<{ acct: string }>
  media_attachments?: MastodonMedia[]
  in_reply_to_id?: string
}

interface MastodonMedia {
  id: string
  type: 'image' | 'video' | 'gifv' | 'audio'
  url: string
  preview_url?: string
  description?: string
  meta?: {
    original?: {
      width: number
      height: number
    }
    duration?: number
  }
}

interface BlueskyPost {
  uri: string
  author: {
    did: string
    handle: string
    displayName?: string
    avatar?: string
    followersCount?: number
  }
  record: {
    text: string
    createdAt: string
  }
  likeCount: number
  replyCount: number
  repostCount: number
  embed?: any
}

interface ActivityPubClient {
  searchInteractions(targetUrl: string): Promise<ActivityPubActivity[]>
}

interface ActivityPubActivity {
  id: string
  type: string
  object: ActivityPubObject
}

interface ActivityPubObject {
  id: string
  url?: string
  content: string
  published: string
  attributedTo: {
    id: string
    name: string
    preferredUsername: string
    icon?: { url: string }
    url: string
    followersCount?: number
  }
  likeCount?: number
  shareCount?: number
  replyCount?: number
  tag?: Array<{ type: string; name: string }>
}

export default SocialNetworkAggregator

Herramientas

Herramientas que suelen usarse con este ejemplo

Relacionado

Colecciones de ejemplos relacionadas

Relacionado

Temas relacionados

Guía de uso de la herramienta

Contexto para entender esta colección de ejemplos

Título

Interacciones WebMentions entre Sitios

Descripción

Ejemplos del protocolo WebMentions para interacciones entre sitios, sistemas de comentarios y redes sociales

Resumen

Ejemplos del protocolo WebMentions para interacciones entre sitios, sistemas de comentarios y redes sociales

Cuándo usarlo

  • Use this sample set when you want reference inputs for Interacciones WebMentions entre Sitios.
  • Use it to preview output formats and compare related sample variants.

Cómo funciona

  1. 1Open a sample entry to inspect the prepared example payload or asset.
  2. 2Compare it with related tools to understand how the workflow connects.

Casos de uso

  • WebMentions Hello World
  • Sistema de Comentarios Avanzado
  • Agregador de Redes Sociales

Preguntas frecuentes

What is included in Interacciones WebMentions entre Sitios?

This page includes curated sample entries, related tools, and neighboring sample collections for Interacciones WebMentions entre Sitios.

Tema

Conversión de formatos para entrega de video: audio, streams, GIF, WebM y MP4

Define el destino de entrega, extrae solo el stream necesario y convierte animaciones breves, dejando constancia de la compatibilidad, la calidad y el tamaño resultantes.

Tema

Ajuste de codificación de imágenes para peso, carga y entrega

Ajusta la profundidad de bits, la paleta y la transparencia del PNG, elige la carga progresiva o de línea base del JPEG, configura los modos de calidad de WebP y comprime el TIFF de archivo hasta que cada imagen quepa en su destino.

Tema

Captura autorizada de webhooks y revisión de confianza del endpoint

Captura y repite solicitudes webhook controladas, revisa la evidencia de entrega y comprueba el certificado HTTPS del receptor sin confundir un certificado válido con confianza empresarial.

Tema

Inventario de estilos web y revisión de tokens de diseño

Captura una página pública autorizada, contrasta estilos con la imagen, revisa pares de colores y cambios de tokens y entrega pruebas sin atribuirles el sistema de diseño original.

Inicio
Herramientas3201
Ejemplos593
Visualizaciones465
Visores de archivos103
Temas233
Etiquetas333
Imágenes, audio y video657

Herramientas de procesamiento, conversión y optimización de imágenes, audio y video

Matemáticas, fechas y finanzas644

Calculadoras, herramientas numéricas, lógica de fechas, estadística y finanzas

Diseño y color297

Herramientas de color, maquetación, gráficos, estilo visual y apoyo al diseño

Texto y redacción209

Herramientas de redacción, limpieza, formato, extracción y análisis de texto

Desarrollo y Web193

Utilidades para desarrolladores, red, depuración web y automatización

Conversión y codificación175

Herramientas de conversión de formatos, archivos, unidades, archivos comprimidos y codificación

Revisión de fuentes FLAC/WAV y entrega de evidencias
Revisión e integración de un componente de sorteos autoalojado
Revisión de lotes de códigos Stripe y comprobación previa a la importación
Revisión del descarte fotográfico y entrega de candidatos para edición
Estructurar una captura de interfaz, revisarla y entregarla a desarrollo

© 2026 Elysia Tools. Todos los derechos reservados.

DocumentaciónPreciosAcerca deTérminos de servicioPolítica de privacidadCookiesContacto

Media

Convertidor de MP4 a WebM

Convierte archivos de video MP4 a formato WebM

Media

Remux inteligente

Reenvuelve contenedores con stream copy.

Media

Conversor de Video en Lote

Convierte múltiples archivos de video entre diferentes formatos como MP4, WebM, MKV, AVI, MOV con configuración de calidad y procesamiento por lotes

Media

Video a GIF Optimizado

Convierte videos a formato GIF optimizado con configuración avanzada de compresión

Media

Cortador de Video

Cortar y recortar clips de video con precisión, soportando múltiples formatos y configuraciones de calidad

Media

Volteo Horizontal de Video

Refleja el video de izquierda a derecha, conservando formato contenedor y audio cuando sea posible

Media

Extractor de Streams de Video

Detecta streams primero y extrae audio, video sin audio o ambos sin transcodificar

Media

Convertidor de Video a AAC

Extrae audio de video y lo convierte a AAC

Medios

Ejemplos de Video VP9

Archivos de video de muestra codificados en formato VP9

Medios

Ejemplos SVG

Ejemplos de Gráficos Vectoriales Escalables (SVG) que demuestran varias características y técnicas SVG

Python

Ejemplos de Características Móviles Web Python

Ejemplos de características móviles Web Python incluyendo información del dispositivo, estado de red y vibración

TypeScript

Ejemplos de Características Móviles Web TypeScript

Ejemplos de características móviles Web TypeScript incluyendo información del dispositivo, estado de red y vibración

Procesamiento de código

Ejemplos de Comentarios de Código

Archivos de muestra de código con varios estilos de comentarios para probar la eliminación de comentarios

Pruebas

BDD with Cucumber - Prácticas de Desarrollo Orientado a Comportamiento

Ejemplos completos de Cucumber BDD incluyendo archivos de características, definiciones de pasos, tablas de datos, hooks y patrones BDD avanzados para desarrollo colaborativo