UNPKG

apptise-core

Version:

Core library for Apptise unified notification system

325 lines 12 kB
import { NotificationPlugin } from '../base/plugin.js'; import { HttpClient } from '../utils/http-client.js'; // Accept: // - @username // - username // - username@host.com // - @username@host.com const IS_USER = /^\s*@?(?<user>[A-Z0-9_]+(?:@(?<host>[A-Z0-9_.-]+))?)$/i; const USER_DETECTION_RE = /(@[A-Z0-9_]+(?:@[A-Z0-9_.-]+)?)(?=$|[\s,.&()\[\]]+)/gi; // Message visibility options const MASTODON_MESSAGE_VISIBILITIES = [ 'default', 'direct', 'private', 'unlisted', 'public', ]; // Mastodon API endpoints const MASTODON_ENDPOINTS = { whoami: '/api/v1/accounts/verify_credentials', media: '/api/v1/media', toot: '/api/v1/statuses', dm: '/api/v1/dm', }; export class MastodonPlugin extends NotificationPlugin { registration = { serviceId: 'mastodon', protocols: ['mastodon', 'toot', 'mastodons', 'toots'], name: 'Mastodon', description: 'Mastodon social network notifications', version: '1.0.0', }; serviceConfig = { name: 'Mastodon', url: 'https://joinmastodon.org', setupUrl: 'https://github.com/caronc/apprise/wiki/Notify_mastodon', timeout: 10000, userAgent: 'Apptise/1.0.0 Mastodon Plugin', maxBodyLength: 500, maxTitleLength: 0, // Title is not used attachmentSupport: true, imageSize: '128x128', maxImagesPerToot: 4, }; templates = [ '{schema}://{token}@{host}', '{schema}://{token}@{host}:{port}', '{schema}://{token}@{host}/{targets}', '{schema}://{token}@{host}:{port}/{targets}', ]; templateTokens = { host: { name: 'Hostname', type: 'string', required: true, }, token: { name: 'Access Token', type: 'string', required: true, private: true, }, port: { name: 'Port', type: 'int', min: 1, max: 65535, }, target_user: { name: 'Target User', type: 'string', prefix: '@', mapTo: 'targets', }, targets: { name: 'Targets', type: 'list:string', }, }; templateArgs = { token: { aliasOf: 'token', }, visibility: { name: 'Visibility', type: 'choice:string', values: MASTODON_MESSAGE_VISIBILITIES, default: 'default', }, cache: { name: 'Cache Results', type: 'bool', default: true, }, batch: { name: 'Batch Mode', type: 'bool', default: true, }, sensitive: { name: 'Sensitive Attachments', type: 'bool', default: false, }, spoiler: { name: 'Spoiler Text', type: 'string', }, key: { name: 'Idempotency-Key', type: 'string', }, language: { name: 'Language Code', type: 'string', }, to: { aliasOf: 'targets', }, }; // Cache for whoami results whoamiCache = null; rateLimitRemaining = 1; rateLimitReset = new Date(); parseUrl(url) { try { const parsedUrl = new URL(url); // Determine if secure protocol const secure = parsedUrl.protocol.endsWith('s:'); // Extract host const host = parsedUrl.hostname; if (!host) { throw new Error('Host is required'); } const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : undefined; // Extract token from username or query parameter let token = parsedUrl.username || undefined; const params = new URLSearchParams(parsedUrl.search); if (params.get('token')) { token = params.get('token'); } if (!token) { throw new Error('Access token is required'); } // Extract targets from path const pathSegments = parsedUrl.pathname.split('/').filter(segment => segment.length > 0); const targets = []; // Validate and process targets for (const segment of pathSegments) { const match = IS_USER.exec(segment); if (match && match.groups?.user) { targets.push('@' + match.groups.user); } else if (segment.trim()) { console.warn(`Dropped invalid Mastodon user (${segment}) specified.`); } } // Handle 'to' parameter as additional targets const toParam = params.get('to'); if (toParam) { const toTargets = toParam.split(',').map(t => t.trim()).filter(t => t); for (const target of toTargets) { const match = IS_USER.exec(target); if (match && match.groups?.user) { targets.push('@' + match.groups.user); } } } // Parse visibility let visibility = 'default'; const visibilityParam = params.get('visibility'); if (visibilityParam) { const vis = visibilityParam.toLowerCase().trim(); const foundVisibility = MASTODON_MESSAGE_VISIBILITIES.find(v => v.startsWith(vis)); if (foundVisibility) { visibility = foundVisibility; } else { throw new Error(`Invalid Mastodon visibility specified (${visibilityParam})`); } } else if (parsedUrl.protocol.startsWith('toot')) { visibility = 'public'; } // Parse other parameters const batch = params.get('batch') !== 'no' && params.get('batch') !== 'false'; const sensitive = params.get('sensitive') === 'yes' || params.get('sensitive') === 'true'; const cache = params.get('cache') !== 'no' && params.get('cache') !== 'false'; const spoiler = params.get('spoiler') || undefined; const key = params.get('key') || undefined; const language = params.get('language') || undefined; return { serviceId: this.registration.serviceId, url, config: { token, host, port, secure, targets, visibility, batch, sensitive, spoiler, key, language, cache, }, }; } catch (error) { throw new Error(`Failed to parse Mastodon URL: ${error instanceof Error ? error.message : String(error)}`); } } async send(config, message) { const mastodonConfig = config.config; try { // Prepare the base URL const protocol = mastodonConfig.secure ? 'https' : 'http'; const portSuffix = mastodonConfig.port ? `:${mastodonConfig.port}` : ''; const apiUrl = `${protocol}://${mastodonConfig.host}${portSuffix}`; // Prepare headers const headers = { 'Content-Type': 'application/json', 'User-Agent': this.serviceConfig.userAgent, 'Authorization': `Bearer ${mastodonConfig.token}`, }; // Smart Target Detection for Direct Messages const users = new Set(); if (message.body) { const userMatches = message.body.matchAll(USER_DETECTION_RE); for (const match of userMatches) { users.add(match[1]); } } let targets = Array.from(users).filter(user => !mastodonConfig.targets.includes(user)); if (!mastodonConfig.targets.length && mastodonConfig.visibility === 'direct') { // Get current user info for direct messages const whoamiResult = await this.whoami(apiUrl, headers, mastodonConfig.cache); if (whoamiResult) { const myself = '@' + Object.keys(whoamiResult)[0]; if (users.has(myself)) { targets = targets.filter(t => t !== myself); } else { targets.push(myself); } } } // Prepare payload // Combine title and body like Python version: "title\r\nbody" let statusContent = ''; if (message.title && message.body) { statusContent = `${message.title}\r\n${message.body}`; } else { statusContent = message.body || message.title || ''; } const payload = { status: targets.length > 0 ? `${targets.join(' ')} ${statusContent}` : statusContent, sensitive: mastodonConfig.sensitive, }; // Handle Visibility Flag if (mastodonConfig.visibility !== 'default') { payload.visibility = mastodonConfig.visibility; } // Set optional parameters if (mastodonConfig.spoiler) { payload.spoiler_text = mastodonConfig.spoiler; } if (mastodonConfig.key) { payload['Idempotency-Key'] = mastodonConfig.key; } if (mastodonConfig.language) { payload.language = mastodonConfig.language; } // Send the toot const response = await HttpClient.post(`${apiUrl}${MASTODON_ENDPOINTS.toot}`, JSON.stringify(payload), { headers, timeout: this.serviceConfig.timeout }); if (response.status === 200 || response.status === 201) { return { success: true, serviceId: this.registration.serviceId, }; } else { console.error(`Failed to send Mastodon notification: HTTP ${response.status}`); return { success: false, serviceId: this.registration.serviceId, error: `HTTP ${response.status}`, }; } } catch (error) { console.error('Error sending Mastodon notification:', error); return { success: false, serviceId: this.registration.serviceId, error: error instanceof Error ? error.message : String(error), }; } } async whoami(apiUrl, headers, useCache = true) { if (useCache && this.whoamiCache) { return this.whoamiCache; } try { const response = await HttpClient.get(`${apiUrl}${MASTODON_ENDPOINTS.whoami}`, { headers, timeout: this.serviceConfig.timeout }); if (response.status === 200 && response.data) { const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; if (data.username && data.id) { this.whoamiCache = { [data.username]: data.id }; return this.whoamiCache; } } } catch (error) { console.error('Failed to get Mastodon user info:', error); } return null; } } export const mastodonPlugin = new MastodonPlugin(); export default mastodonPlugin; //# sourceMappingURL=mastodon.js.map