From 2e5610555e24023508332f5e6104c56a89eb79bc Mon Sep 17 00:00:00 2001 From: Kolkman Date: Wed, 26 Aug 2026 09:51:28 +0200 Subject: [PATCH] Format mastodon output --- README.md | 2 +- VIBE/CHAT_LOG.md | 12 ++++++++++++ VIBE/PROMPTS.md | 2 ++ backend/app/services/plugin_manager.py | 17 +++++++++-------- backend/tests/test_api.py | 25 +++++++++++++++++++++++-- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dad409b..198baf1 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ For a real deployment, start with the docker-compose-example.yaml file. replace ## Mastodon Configuration -After logging in, open , enter the Mastodon instance, and select **Connect Mastodon**. LinkLog registers an OAuth application on that instance, opens Mastodon authorization, and stores the returned per-user access token after the callback. The requested scopes are `read:accounts` and `write:statuses`. +After logging in, open , enter the Mastodon instance, and select **Connect Mastodon**. LinkLog registers an OAuth application on that instance, opens Mastodon authorization, and stores the returned per-user access token after the callback. The requested scopes are `read:accounts` and `write:statuses`. Posts use the configured prefix followed directly by the title, an optional comment, a `from: URL` line when a title exists, and tags on the final line, with blank lines between sections. - **Instance**: hostname or URL such as `mastodon.social` or `https://mastodon.social` - **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: ` diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index 46da8e5..a5de1e2 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -628,3 +628,15 @@ Changed Mastodon status publishing to form-encoded API requests, added safe conf ### Assistant outcome Removed the duplicate `TRAEFIK_HOST` configuration, changed both Traefik router rules to use `LINKLOG_PUBLIC_URL`, standardized the value as a host name, and normalized scheme-less values to valid HTTP/HTTPS backend callback URLs. Compose validation, URL normalization, and the complete Dockerized backend suite passed with 22 tests. + +### User +Format Mastodon posts as `From my #LinkLog:`, the web page title, an optional comment, and `from: URL`. + +### Assistant outcome +Updated the Mastodon formatter to emit the configured prefix, title, optional comment, and source URL as blank-line-separated blocks. Tags remain on the source URL line. The focused posting test and complete Dockerized backend suite passed with 22 tests, and the running container is healthy on port 5469. + +### User +Only print the `from` line if there is no title. Put the title directly behind the colon, and put all tags on the last line. + +### Assistant outcome +Updated Mastodon formatting to place the title directly after `From my #LinkLog:`, omit the `from:` line when no title is available, and place tags on a separate final line. Added no-title regression coverage. The focused tests passed and the complete Dockerized backend suite passed with 23 tests. diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index 6abeac2..1c84f37 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -115,6 +115,8 @@ 109. use VIVE that is in the current directory 110. Posting to mastodon seems to fail, try to fix, add debug logging 111. LINKLOG_PUBLIC_URL and TRAEFIK_HOST are the same and can be merged. (use LINKLOG_PUBLIC_URL), fix docker-compose to use said variable +112. Format mastodon posts like From my #LinkLog: Title, optional comment, and from: URL +113. Only print the 'from' line if there is no title. Put the title directly behind the colon, and put all tags on the last line. ## Future entries diff --git a/backend/app/services/plugin_manager.py b/backend/app/services/plugin_manager.py index 7c1f3a0..a388e94 100644 --- a/backend/app/services/plugin_manager.py +++ b/backend/app/services/plugin_manager.py @@ -59,18 +59,19 @@ class MastodonPlugin(BasePlugin): ) return {'status': 'skipped', 'plugin': self.name, 'reason': 'not_configured'} - status_parts = [event.get('title') or event.get('url', '')] - if event.get('comment'): - status_parts.append(event['comment']) 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) - status = f'{post_prefix}{event.get("url", "")}'.strip() + 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 = f'{status} {" ".join(event["tags"])}' - status_parts.append(status) - post_body = '\n'.join(status_parts) + 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', diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 69a4a41..5489c48 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -6,7 +6,7 @@ import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs from uuid import uuid4 -from unittest.mock import patch +from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient @@ -445,13 +445,34 @@ def test_link_submission_posts_to_enabled_mastodon_plugin(): assert received['path'] == '/api/v1/statuses' assert received['authorization'] == 'Bearer test-token' assert received['content_type'] == 'application/x-www-form-urlencoded' - assert received['body'] == {'status': ['A useful page\nWorth sharing\nFrom my #LinkLog: https://example.com/useful #python #web']} + assert received['body'] == {'status': ['From my #LinkLog: A useful page\n\nWorth sharing\n\nfrom: https://example.com/useful\n\n#python #web']} finally: server.shutdown() thread.join() server.server_close() +def test_mastodon_post_without_title_omits_source_line(): + from backend.app.services.plugin_manager import MastodonPlugin + + response = MagicMock() + response.__enter__.return_value.read.return_value = b'{"id":"status-2"}' + plugin = MastodonPlugin() + plugin.initialize({'instance': 'https://mastodon.example', 'access_token': 'test-token'}) + + with patch('backend.app.services.plugin_manager.urlopen', return_value=response) as open_url: + result = plugin.handle_event({ + 'url': 'https://example.com/useful', + 'title': '', + 'comment': 'A comment', + 'tags': ['#tag'], + }) + + body = parse_qs(open_url.call_args.args[0].data.decode())['status'][0] + assert result['status'] == 'posted' + assert body == 'From my #LinkLog:\n\nA comment\n\n#tag' + + def test_plugin_config_can_be_saved_for_mastodon(): headers = login_headers() assert client.put('/api/user/plugins/mastodon', headers=headers, json={