155 lines
5.8 KiB
Python
155 lines
5.8 KiB
Python
## Copyright © 2026 Olaf Kolkman
|
|
## SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import json
|
|
import logging
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.parse import urlencode
|
|
from urllib.request import Request, urlopen
|
|
|
|
from backend.app.plugins.base import BasePlugin
|
|
|
|
DEFAULT_POST_PREFIX = 'From my #LinkLog: '
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DefaultFrontendPlugin(BasePlugin):
|
|
name = 'default_frontend'
|
|
version = '1.0.0'
|
|
|
|
def handle_event(self, event):
|
|
return {'status': 'accepted', 'plugin': self.name, 'event': event.get('type')}
|
|
|
|
|
|
class MastodonPlugin(BasePlugin):
|
|
name = 'mastodon'
|
|
version = '1.0.0'
|
|
enabled = False
|
|
config = {}
|
|
|
|
def initialize(self, config=None):
|
|
self.config = config or {}
|
|
return True
|
|
|
|
def handle_event(self, event):
|
|
config = dict(self.config)
|
|
user_id = event.get('user_id')
|
|
if user_id:
|
|
from backend.app.database import get_connection
|
|
|
|
with get_connection() as conn:
|
|
row = conn.execute(
|
|
'''
|
|
SELECT config FROM user_plugin_config
|
|
WHERE user_id = ? AND plugin_name = ?
|
|
''',
|
|
(user_id, self.name),
|
|
).fetchone()
|
|
if row and row['config']:
|
|
config.update(json.loads(row['config']))
|
|
|
|
instance = str(config.get('instance', '')).strip().rstrip('/')
|
|
if instance and '://' not in instance:
|
|
instance = f'https://{instance}'
|
|
access_token = str(config.get('access_token', '')).strip()
|
|
if not instance or not access_token:
|
|
logger.debug(
|
|
'Mastodon post skipped: instance_configured=%s token_configured=%s user_id=%s',
|
|
bool(instance), bool(access_token), user_id,
|
|
)
|
|
return {'status': 'skipped', 'plugin': self.name, 'reason': 'not_configured'}
|
|
|
|
post_prefix = config.get('post_prefix')
|
|
if post_prefix is None and config.get('hashtag'):
|
|
post_prefix = f'#{str(config["hashtag"]).strip().lstrip("#")} '
|
|
post_prefix = str(post_prefix if post_prefix is not None else DEFAULT_POST_PREFIX).strip()
|
|
title = str(event.get('title') or '').strip()
|
|
status_parts = [f'{post_prefix} {title}'.strip()]
|
|
if event.get('comment'):
|
|
status_parts.append(event['comment'])
|
|
if title:
|
|
status_parts.append(f'from: {event.get("url", "")}')
|
|
if event.get('tags'):
|
|
status_parts.append(' '.join(event['tags']))
|
|
post_body = '\n\n'.join(status_parts)
|
|
endpoint = f'{instance}/api/v1/statuses'
|
|
logger.debug(
|
|
'Posting link to Mastodon: endpoint=%s user_id=%s event_id=%s body_length=%d',
|
|
endpoint, user_id, event.get('id'), len(post_body),
|
|
)
|
|
|
|
try:
|
|
request = Request(
|
|
endpoint,
|
|
data=urlencode({'status': post_body}).encode('utf-8'),
|
|
headers={
|
|
'Authorization': f'Bearer {access_token}',
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Accept': 'application/json',
|
|
'User-Agent': 'LinkLog/1.0',
|
|
},
|
|
method='POST',
|
|
)
|
|
with urlopen(request, timeout=5) as response:
|
|
response_body = response.read().decode('utf-8')
|
|
logger.debug(
|
|
'Mastodon post response: endpoint=%s status=%s body_length=%d',
|
|
endpoint, response.status, len(response_body),
|
|
)
|
|
response_data = json.loads(response_body)
|
|
logger.info('Mastodon post succeeded: instance=%s user_id=%s post_id=%s', instance, user_id, response_data.get('id'))
|
|
return {
|
|
'status': 'posted',
|
|
'plugin': self.name,
|
|
'post_id': response_data.get('id'),
|
|
}
|
|
except HTTPError as error:
|
|
response_body = error.read().decode('utf-8', errors='replace')
|
|
logger.warning(
|
|
'Mastodon post failed: endpoint=%s user_id=%s status=%s response=%s',
|
|
endpoint, user_id, error.code, response_body[:500],
|
|
)
|
|
return {
|
|
'status': 'failed',
|
|
'plugin': self.name,
|
|
'reason': f'HTTP {error.code}: {response_body[:500]}',
|
|
}
|
|
except (URLError, TimeoutError, OSError, ValueError) as error:
|
|
logger.exception('Mastodon post failed: endpoint=%s user_id=%s error=%s', endpoint, user_id, error)
|
|
return {
|
|
'status': 'failed',
|
|
'plugin': self.name,
|
|
'reason': str(error),
|
|
}
|
|
|
|
|
|
class PluginManager:
|
|
def __init__(self):
|
|
self.plugins = [DefaultFrontendPlugin(), MastodonPlugin()]
|
|
|
|
def refresh_from_db(self):
|
|
from backend.app.database import get_connection
|
|
|
|
with get_connection() as conn:
|
|
rows = conn.execute('SELECT name, enabled, config FROM plugins').fetchall()
|
|
|
|
enabled_names = {row['name'] for row in rows if row['enabled']}
|
|
for plugin in self.plugins:
|
|
plugin.enabled = plugin.name in enabled_names
|
|
row = next((item for item in rows if item['name'] == plugin.name), None)
|
|
plugin.initialize(json.loads(row['config']) if row and row['config'] else {})
|
|
return enabled_names
|
|
|
|
def dispatch(self, event):
|
|
self.refresh_from_db()
|
|
results = []
|
|
for plugin in self.plugins:
|
|
if plugin.enabled:
|
|
result = plugin.handle_event(event)
|
|
results.append(result)
|
|
logger.debug('Plugin dispatch result: plugin=%s event_id=%s result=%s', plugin.name, event.get('id'), result)
|
|
return results
|
|
|
|
|
|
plugin_manager = PluginManager()
|