apptise-core
Version:
Core library for Apptise unified notification system
325 lines • 12 kB
JavaScript
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