Post to mastodon from the profile page for logged in users

This commit is contained in:
2026-08-26 10:20:48 +02:00
parent 9deb28330a
commit 95accdf436
13 changed files with 136 additions and 5 deletions
+31 -1
View File
@@ -5,7 +5,8 @@ from fastapi import APIRouter, Header, HTTPException, status
import logging
from pydantic import BaseModel
from backend.app.services.link_service import create_link, delete_link, list_public_links, list_tags, update_link
from backend.app.services.link_service import create_link, delete_link, get_link_tags, list_public_links, list_tags, mark_mastodon_posted, update_link
from backend.app.database import get_connection
from backend.app.services.plugin_manager import plugin_manager
from backend.app.services.token_service import validate_token
@@ -47,6 +48,9 @@ def create_link_endpoint(payload: LinkCreate, authorization: str | None = Header
except ValueError as error:
raise HTTPException(status_code=422, detail=str(error)) from error
plugin_results = plugin_manager.dispatch({'type': 'link_created', **record})
mastodon_result = next((result for result in plugin_results if result.get('plugin') == 'mastodon'), None)
if mastodon_result and mastodon_result.get('status') == 'posted':
mark_mastodon_posted(record['id'], info['user_id'], mastodon_result.get('post_id'))
if any(result.get('status') == 'failed' for result in plugin_results):
logger.warning('One or more plugins failed for link_id=%s results=%s', record['id'], plugin_results)
return record
@@ -88,6 +92,32 @@ def delete_link_endpoint(
return {'status': 'deleted', 'id': link_id}
@router.post('/links/{link_id}/mastodon')
def post_link_to_mastodon(
link_id: str,
authorization: str | None = Header(default=None),
):
if not authorization or not authorization.startswith('Bearer '):
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
info = validate_token(authorization.replace('Bearer ', '', 1))
if info is None:
raise HTTPException(status_code=401, detail='Token expired or invalid')
with get_connection() as conn:
row = conn.execute('SELECT * FROM links WHERE id = ? AND user_id = ?', (link_id, info['user_id'])).fetchone()
if row is None:
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
event = dict(row)
with get_connection() as conn:
event['tags'] = get_link_tags(conn, link_id)
result = plugin_manager.post_to_mastodon({'type': 'link_created', **event})
if result.get('status') == 'posted':
mark_mastodon_posted(link_id, info['user_id'], result.get('post_id'))
return {'status': 'posted', 'post_id': result.get('post_id')}
if result.get('status') == 'skipped':
raise HTTPException(status_code=409, detail='Mastodon is not enabled or configured')
raise HTTPException(status_code=502, detail=result.get('reason', 'Mastodon post failed'))
@router.get('/links')
def list_links():
return list_public_links()
+1
View File
@@ -43,6 +43,7 @@ def public_feed(
'is_owner': item['user_id'] == current_user_id,
'can_edit': item['user_id'] == current_user_id,
'created_at': item['created_at'],
'mastodon_posted': bool(item['mastodon_posted']),
}
for item in items
]
+5
View File
@@ -151,6 +151,11 @@ CREATE TABLE IF NOT EXISTS mastodon_oauth_states (
);
CREATE INDEX IF NOT EXISTS idx_mastodon_oauth_states_state_hash
ON mastodon_oauth_states(state_hash);
'''),
(9, '''
ALTER TABLE links ADD COLUMN mastodon_posted INTEGER NOT NULL DEFAULT 0;
ALTER TABLE links ADD COLUMN mastodon_post_id TEXT;
ALTER TABLE links ADD COLUMN mastodon_posted_at TEXT;
'''),
]
+12
View File
@@ -167,6 +167,18 @@ def delete_link(link_id: str, user_id: str) -> bool:
return cursor.rowcount > 0
def mark_mastodon_posted(link_id: str, user_id: str, post_id: str | None) -> bool:
with get_connection() as conn:
cursor = conn.execute(
'''UPDATE links
SET mastodon_posted = 1, mastodon_post_id = ?, mastodon_posted_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?''',
(post_id, link_id, user_id),
)
conn.commit()
return cursor.rowcount > 0
def list_public_users():
with get_connection() as conn:
rows = conn.execute(
+7
View File
@@ -150,5 +150,12 @@ class PluginManager:
logger.debug('Plugin dispatch result: plugin=%s event_id=%s result=%s', plugin.name, event.get('id'), result)
return results
def post_to_mastodon(self, event):
self.refresh_from_db()
plugin = next(plugin for plugin in self.plugins if plugin.name == 'mastodon')
if not plugin.enabled:
return {'status': 'skipped', 'plugin': 'mastodon', 'reason': 'disabled'}
return plugin.handle_event(event)
plugin_manager = PluginManager()
+3
View File
@@ -403,6 +403,7 @@ def test_public_and_admin_pages_render_html():
feed_script = client.get('/static/feed.js?v=7').text
assert 'if (item.is_owner && !showIdentity)' in feed_script
assert 'deleteEntry(item, deleteButton)' in feed_script
assert 'postToMastodon(item, mastodonButton)' in feed_script
assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script
assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script
assert 'edit-tag-options' in feed_script
@@ -452,6 +453,8 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
assert received['authorization'] == 'Bearer test-token'
assert received['content_type'] == 'application/x-www-form-urlencoded'
assert received['body'] == {'status': ['From my #LinkLog: A useful page\n\nWorth sharing\n\nfrom: https://example.com/useful\n\n#python #web']}
posted_item = next(item for item in client.get('/api/public/feed/alice', headers=headers).json() if item['id'] == response.json()['id'])
assert posted_item['mastodon_posted'] is True
finally:
server.shutdown()
thread.join()
+2 -2
View File
@@ -10,7 +10,7 @@ def test_database_migrations_are_versioned_and_idempotent():
connection = sqlite3.connect(':memory:')
apply_migrations(connection)
assert get_schema_version(connection) == 8
assert get_schema_version(connection) == 9
tables = {
row[0]
for row in connection.execute(
@@ -27,6 +27,6 @@ def test_database_migrations_are_versioned_and_idempotent():
assert set(DEFAULT_TAGS) <= seeded_tags
apply_migrations(connection)
assert get_schema_version(connection) == 8
assert get_schema_version(connection) == 9
connection.close()