OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / hyper-social / hyper-social-web / lib / clients / mastodon-client.js

124 lines · javascript ref:
import https from 'bare-https'

class MastodonClient {
  constructor (opts = {}) {
    this.instance = opts.instance
    this.token = opts.token
  }

  _request (method, path, body) {
    return new Promise((resolve, reject) => {
      const opts = {
        hostname: this.instance,
        port: 443,
        path,
        method,
        headers: {
          Authorization: `Bearer ${this.token}`,
          Accept: 'application/json'
        }
      }

      let payload = ''
      if (body) {
        payload = JSON.stringify(body)
        opts.headers['Content-Type'] = 'application/json'
        opts.headers['Content-Length'] = Buffer.byteLength(payload)
      }

      const req = https.request(opts, (res) => {
        let data = ''
        res.on('data', (chunk) => { data += chunk.toString() })
        res.on('end', () => {
          if (res.statusCode >= 400) {
            return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`))
          }
          try { resolve(JSON.parse(data)) } catch { reject(new Error('Invalid JSON')) }
        })
      })

      req.on('error', reject)
      req.setTimeout(15000, () => req.destroy(new Error('Request timeout')))
      if (payload) req.write(payload)
      req.end()
    })
  }

  async postStatus (text, opts = {}) {
    const body = { status: text }
    if (opts.visibility) body.visibility = opts.visibility
    if (opts.spoilerText) body.spoiler_text = opts.spoilerText
    if (opts.inReplyTo) body.in_reply_to_id = opts.inReplyTo
    if (opts.mediaIds && opts.mediaIds.length) body.media_ids = opts.mediaIds
    return this._request('POST', '/api/v1/statuses', body)
  }

  // Upload media via multipart/form-data
  async uploadMedia (buffer, mimeType, description) {
    return new Promise((resolve, reject) => {
      const boundary = '----HyperSocial' + Date.now().toString(36)
      const parts = []

      // File part
      parts.push(`--${boundary}\r\n`)
      parts.push(`Content-Disposition: form-data; name="file"; filename="media.${mimeType.split('/')[1] || 'jpg'}"\r\n`)
      parts.push(`Content-Type: ${mimeType}\r\n\r\n`)
      const header = Buffer.from(parts.join(''))

      const footer = Buffer.from(`\r\n`)
      let descPart = Buffer.alloc(0)
      if (description) {
        descPart = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="description"\r\n\r\n${description}\r\n`)
      }
      const end = Buffer.from(`--${boundary}--\r\n`)

      const payload = Buffer.concat([header, buffer, footer, descPart, end])

      const req = https.request({
        hostname: this.instance,
        port: 443,
        path: '/api/v2/media',
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.token}`,
          Accept: 'application/json',
          'Content-Type': `multipart/form-data; boundary=${boundary}`,
          'Content-Length': payload.length
        }
      }, (res) => {
        let data = ''
        res.on('data', (chunk) => { data += chunk.toString() })
        res.on('end', () => {
          if (res.statusCode >= 400) {
            return reject(new Error(`Upload ${res.statusCode}: ${data.slice(0, 200)}`))
          }
          try { resolve(JSON.parse(data)) } catch { reject(new Error('Invalid JSON')) }
        })
      })

      req.on('error', reject)
      req.setTimeout(60000, () => req.destroy(new Error('Upload timeout')))
      req.write(payload)
      req.end()
    })
  }

  async homeTimeline (limit = 20, sinceId) {
    let path = `/api/v1/timelines/home?limit=${limit}`
    if (sinceId) path += `&since_id=${sinceId}`
    return this._request('GET', path)
  }

  async notifications (limit = 15, sinceId) {
    let path = `/api/v1/notifications?limit=${limit}`
    if (sinceId) path += `&since_id=${sinceId}`
    return this._request('GET', path)
  }

  async verifyCredentials () {
    return this._request('GET', '/api/v1/accounts/verify_credentials')
  }
}

export { MastodonClient }