OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / hyper-social / bridge-mastodon / lib / adapter.js

142 lines · javascript ref:
import { createEnvelope, TYPES, PLATFORMS } from 'bridge-envelope'

// Strip HTML tags from Mastodon status content
function stripHtml (html) {
  return html
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/p>\s*<p>/gi, '\n\n')
    .replace(/<[^>]+>/g, '')
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .trim()
}

// Convert Mastodon status to bridge envelope
function statusToEnvelope (status) {
  if (!status) return null

  // Skip boosts — handle separately if needed
  const actual = status.reblog || status
  const author = actual.account?.acct || actual.account?.username || 'unknown'
  const content = stripHtml(actual.content || '')

  if (!content && !actual.media_attachments?.length) return null

  let type = TYPES.TEXT
  let mediaUrl = ''
  let mediaType = ''
  let replyTo = ''

  // Media attachments
  const attachment = actual.media_attachments?.[0]
  if (attachment) {
    type = TYPES.MEDIA
    mediaUrl = attachment.url || attachment.preview_url || ''
    mediaType = attachment.type || '' // image, video, gifv, audio
  }

  // Reply
  if (actual.in_reply_to_id) {
    replyTo = actual.in_reply_to_id
    if (type === TYPES.TEXT) type = TYPES.REPLY
  }

  const envelope = createEnvelope({
    type,
    platform: PLATFORMS.MASTODON,
    author,
    content,
    channel: status.reblog ? 'boost' : (actual.visibility || 'public'),
    replyTo,
    mediaUrl,
    mediaType
  })

  // Preserve Mastodon status ID for dedup and threading
  envelope._statusId = actual.id
  envelope._spoilerText = actual.spoiler_text || ''
  envelope._tags = (actual.tags || []).map(t => t.name)
  envelope._isBoost = !!status.reblog
  envelope._boostBy = status.reblog ? (status.account?.acct || '') : ''

  return envelope
}

// Convert Keet message to Mastodon post text
function keetToMastodon (keetMsg, bridgeIdentity) {
  if (!keetMsg) return null

  // Skip own messages (bridge identity)
  if (keetMsg.memberId === bridgeIdentity) return null

  // Skip system messages
  if (keetMsg.type === 'system') return null

  const text = keetMsg.text || ''
  if (!text.trim()) return null

  // Skip messages already bridged from Mastodon
  if (text.includes('[mastodon]') || text.includes('[Mastodon]')) return null

  const name = keetMsg.name || keetMsg.username || 'someone'
  return { author: name, text }
}

// Format envelope for Mastodon posting (from Keet to Mastodon)
function envelopeToStatusText (envelope) {
  if (!envelope) return ''

  const platformName = {
    [PLATFORMS.KEET]: 'Keet',
    [PLATFORMS.IRC]: 'IRC',
    [PLATFORMS.DISCORD]: 'Discord',
    [PLATFORMS.MATRIX]: 'Matrix',
    [PLATFORMS.EMAIL]: 'Email'
  }

  const source = platformName[envelope.platform] || 'P2P'
  return `[${source}] ${envelope.author}: ${envelope.content}`
}

// Format a Mastodon status for display in Keet
function statusToKeetText (envelope) {
  if (!envelope) return ''

  let text = ''
  if (envelope._isBoost) {
    text += `[mastodon] ${envelope._boostBy} boosted @${envelope.author}: `
  } else {
    text += `[mastodon] @${envelope.author}: `
  }

  if (envelope._spoilerText) {
    text += `[CW: ${envelope._spoilerText}] `
  }

  text += envelope.content

  if (envelope._tags?.length) {
    const tags = envelope._tags.map(t => `#${t}`).join(' ')
    if (!envelope.content.includes('#')) {
      text += ` ${tags}`
    }
  }

  if (envelope.mediaUrl) {
    text += ` [${envelope.mediaType || 'media'}: ${envelope.mediaUrl}]`
  }

  return text
}

export {
  statusToEnvelope,
  keetToMastodon,
  envelopeToStatusText,
  statusToKeetText,
  stripHtml
}