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 }