Format mastodon output

This commit is contained in:
2026-08-26 09:51:28 +02:00
parent 22add753fe
commit 2e5610555e
5 changed files with 47 additions and 11 deletions
+1 -1
View File
@@ -225,7 +225,7 @@ For a real deployment, start with the docker-compose-example.yaml file. replace
## Mastodon Configuration ## Mastodon Configuration
After logging in, open <http://localhost:8000/profile>, 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 <http://localhost:8000/profile>, 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` - **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: ` - **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: `
+12
View File
@@ -628,3 +628,15 @@ Changed Mastodon status publishing to form-encoded API requests, added safe conf
### Assistant outcome ### 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. 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.
+2
View File
@@ -115,6 +115,8 @@
109. use VIVE that is in the current directory 109. use VIVE that is in the current directory
110. Posting to mastodon seems to fail, try to fix, add debug logging 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 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 ## Future entries
+9 -8
View File
@@ -59,18 +59,19 @@ class MastodonPlugin(BasePlugin):
) )
return {'status': 'skipped', 'plugin': self.name, 'reason': 'not_configured'} 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') post_prefix = config.get('post_prefix')
if post_prefix is None and config.get('hashtag'): if post_prefix is None and config.get('hashtag'):
post_prefix = f'#{str(config["hashtag"]).strip().lstrip("#")} ' post_prefix = f'#{str(config["hashtag"]).strip().lstrip("#")} '
post_prefix = str(post_prefix if post_prefix is not None else DEFAULT_POST_PREFIX) post_prefix = str(post_prefix if post_prefix is not None else DEFAULT_POST_PREFIX).strip()
status = f'{post_prefix}{event.get("url", "")}'.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'): if event.get('tags'):
status = f'{status} {" ".join(event["tags"])}' status_parts.append(' '.join(event['tags']))
status_parts.append(status) post_body = '\n\n'.join(status_parts)
post_body = '\n'.join(status_parts)
endpoint = f'{instance}/api/v1/statuses' endpoint = f'{instance}/api/v1/statuses'
logger.debug( logger.debug(
'Posting link to Mastodon: endpoint=%s user_id=%s event_id=%s body_length=%d', 'Posting link to Mastodon: endpoint=%s user_id=%s event_id=%s body_length=%d',
+23 -2
View File
@@ -6,7 +6,7 @@ import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs from urllib.parse import parse_qs
from uuid import uuid4 from uuid import uuid4
from unittest.mock import patch from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient 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['path'] == '/api/v1/statuses'
assert received['authorization'] == 'Bearer test-token' assert received['authorization'] == 'Bearer test-token'
assert received['content_type'] == 'application/x-www-form-urlencoded' 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: finally:
server.shutdown() server.shutdown()
thread.join() thread.join()
server.server_close() 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(): def test_plugin_config_can_be_saved_for_mastodon():
headers = login_headers() headers = login_headers()
assert client.put('/api/user/plugins/mastodon', headers=headers, json={ assert client.put('/api/user/plugins/mastodon', headers=headers, json={