MAstodon posts fixed
Build LinkLog Development Image / development-image (push) Successful in 11s

This commit is contained in:
2026-08-26 09:26:16 +02:00
parent 333aab8e8a
commit 22add753fe
12 changed files with 682831 additions and 35 deletions
+2 -1
View File
@@ -11,7 +11,7 @@ LINKLOG_APP_NAME=LinkLog
LINKLOG_VERSION=0.1.0 LINKLOG_VERSION=0.1.0
LINKLOG_SECRET_KEY=replace-with-a-long-random-secret LINKLOG_SECRET_KEY=replace-with-a-long-random-secret
LINKLOG_TOKEN_EXPIRY_DAYS=30 LINKLOG_TOKEN_EXPIRY_DAYS=30
LINKLOG_PUBLIC_URL=http://localhost:8000 LINKLOG_PUBLIC_URL=localhost
LINKLOG_SMTP_HOST= LINKLOG_SMTP_HOST=
LINKLOG_SMTP_PORT=587 LINKLOG_SMTP_PORT=587
LINKLOG_SMTP_USERNAME= LINKLOG_SMTP_USERNAME=
@@ -22,6 +22,7 @@ LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS=24
LINKLOG_PASSWORD_RESET_EXPIRY_HOURS=1 LINKLOG_PASSWORD_RESET_EXPIRY_HOURS=1
LINKLOG_MASTODON_CLIENT_NAME=LinkLog LINKLOG_MASTODON_CLIENT_NAME=LinkLog
LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES=10 LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES=10
LINKLOG_LOG_LEVEL=INFO
# Optional comma-separated override. Leave empty to use the built-in list. # Optional comma-separated override. Leave empty to use the built-in list.
LINKLOG_TRACKING_PARAMS= LINKLOG_TRACKING_PARAMS=
# Keep the default path when using the named linklog_data volume. # Keep the default path when using the named linklog_data volume.
+2 -3
View File
@@ -175,7 +175,7 @@ The main configurable values are:
| `LINKLOG_SECRET_KEY` | token signing/security secret | required in Docker | | `LINKLOG_SECRET_KEY` | token signing/security secret | required in Docker |
| `LINKLOG_DATABASE_PATH` | SQLite file path inside the container | `/app/backend/data/linklog.db` | | `LINKLOG_DATABASE_PATH` | SQLite file path inside the container | `/app/backend/data/linklog.db` |
| `LINKLOG_TOKEN_EXPIRY_DAYS` | access-token lifetime | `30` | | `LINKLOG_TOKEN_EXPIRY_DAYS` | access-token lifetime | `30` |
| `LINKLOG_PUBLIC_URL` | Base URL used in email verification links | `http://localhost:8000` | | `LINKLOG_PUBLIC_URL` | Public hostname used by Traefik and expanded to a callback URL by the backend | `localhost` |
| `LINKLOG_SMTP_HOST` | SMTP server hostname; empty disables delivery in local development | empty | | `LINKLOG_SMTP_HOST` | SMTP server hostname; empty disables delivery in local development | empty |
| `LINKLOG_SMTP_PORT` | SMTP server port | `587` | | `LINKLOG_SMTP_PORT` | SMTP server port | `587` |
| `LINKLOG_SMTP_USERNAME` | SMTP login username | empty | | `LINKLOG_SMTP_USERNAME` | SMTP login username | empty |
@@ -185,7 +185,6 @@ The main configurable values are:
| `LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS` | Verification-link lifetime | `24` | | `LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS` | Verification-link lifetime | `24` |
| `LINKLOG_PASSWORD_RESET_EXPIRY_HOURS` | Password-reset-link lifetime | `1` | | `LINKLOG_PASSWORD_RESET_EXPIRY_HOURS` | Password-reset-link lifetime | `1` |
| `LINKLOG_TRACKING_PARAMS` | comma-separated tracking parameters (stripped from logged URLs) | built-in list | | `LINKLOG_TRACKING_PARAMS` | comma-separated tracking parameters (stripped from logged URLs) | built-in list |
| `TRAEFIK_HOST` | hostname routed by Traefik | `localhost` |
| `APP_PORT` | direct host port for FastAPI | `8000` | | `APP_PORT` | direct host port for FastAPI | `8000` |
New users created by an administrator are email-unverified and cannot sign in until they follow the verification link sent to their address. The link is valid for `LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS` hours and is handled by `/api/auth/verify-email`. Configure `LINKLOG_SMTP_HOST`, `LINKLOG_SMTP_FROM`, and the SMTP credentials for delivery; local development may leave the SMTP host empty, in which case accounts remain pending verification and no message is sent. New users created by an administrator are email-unverified and cannot sign in until they follow the verification link sent to their address. The link is valid for `LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS` hours and is handled by `/api/auth/verify-email`. Configure `LINKLOG_SMTP_HOST`, `LINKLOG_SMTP_FROM`, and the SMTP credentials for delivery; local development may leave the SMTP host empty, in which case accounts remain pending verification and no message is sent.
@@ -231,7 +230,7 @@ After logging in, open <http://localhost:8000/profile>, enter the Mastodon insta
- **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: `
`LINKLOG_PUBLIC_URL` must be the URL users can reach for the OAuth callback. For local development it can remain `http://localhost:8000`; for a deployment it must be the public LinkLog URL. Existing manually entered access tokens remain compatible with the plugin configuration API. `LINKLOG_PUBLIC_URL` is the public hostname used by Traefik and the backend. Use `localhost` for local development or a hostname such as `linklog.kolkman.org` for a deployment. The backend adds `http://` for localhost and `https://` for other hostnames when constructing OAuth and email links. Existing manually entered access tokens remain compatible with the plugin configuration API.
LinkLog caches the OAuth application credentials per Mastodon server in the persistent SQLite `app_settings` table, so subsequent connections do not register a new application on every attempt. If the server rate-limits application registration, the profile page reports the upstream `429` response and the user can retry after the server's cooldown. LinkLog caches the OAuth application credentials per Mastodon server in the persistent SQLite `app_settings` table, so subsequent connections do not register a new application on every attempt. If the server rate-limits application registration, the profile page reports the upstream `429` response and the user can retry after the server's cooldown.
+117
View File
@@ -511,3 +511,120 @@ Add the creation of an xpi bundle for the webextention to the Makefile
### Assistant outcome ### Assistant outcome
Added a portable `xpi` Makefile target that regenerates logo assets and packages the Firefox extension into `LinkLog.xpi`, excluding metadata and minified artifacts. Added README instructions, ignored the generated XPI, and verified the archive contents and integrity with `unzip -tq`. Added a portable `xpi` Makefile target that regenerates logo assets and packages the Firefox extension into `LinkLog.xpi`, excluding metadata and minified artifacts. Added README instructions, ignored the generated XPI, and verified the archive contents and integrity with `unzip -tq`.
### User
Correct: A signed XPI will be generated manually and checked into the repository under XPI/signed
### Assistant outcome
Defined the release workflow around a manually checked-in signed XPI and added version/update-manifest validation, Docker image publishing, and Gitea release asset handling.
### User
The "data_collection_permissions" property is missing from the plugin
### Assistant outcome
Added Firefox `data_collection_permissions` requiring `websiteActivity`, with no optional categories, and enforced the declaration in release validation.
### User
Validate the XPI during the make process of the unsigned xpi
### Assistant outcome
Added unsigned-XPI validation to the Makefile. The validator checks ZIP integrity, required files, macOS metadata, and exact manifest parity.
### User
pressing remove still does not remove a user.
### Assistant outcome
Fixed admin user removal behavior, added server-side self-protection, improved browser error reporting, and updated the admin cache-busting version.
### User
Trying to remove user alice: Could not remove alice: Internal Server Error
### Assistant outcome
Found the SQLite foreign-key failure caused by Alice-owned tokens, links, and plugin configuration. User deletion now removes dependent records transactionally before deleting the user. Full validation passed.
### User
Create a Gitea Action for every update to main that creates a Docker image tagged development
### Assistant outcome
Added `.gitea/workflows/development.yml`, publishing `git.kolkman.org/olaf/link-log:development` on every push to `main`, with manual dispatch support.
### User
Use an access token to publish to registry, not username and password
### Assistant outcome
Changed release and development workflows to use `REGISTRY_TOKEN` as the Docker registry credential while retaining `REGISTRY_USERNAME` as the login identity.
### User
Add SMTP capabilities and email validation links
### Assistant outcome
Added SMTP delivery, email verification tokens, verification-link handling, and environment configuration for SMTP.
### User
Do not configure default users at bootstrap. Present a configuration page when no administrator exists.
### Assistant outcome
Removed default-user bootstrap, added a first-run setup page, persisted SMTP settings, created the first administrator through setup, and added an SMTP test-mail step.
### User
Separate saving configuration from sending test mail, allow five sends with 20-second intervals, then hold for two minutes with a visual timer.
### Assistant outcome
Split setup into Save configuration, Send test mail, and Complete setup actions. Added persistent server-side throttling, reload-safe countdown state, a five-send limit, and a two-minute cooldown.
### User
Use `.env` defaults during initial configuration.
### Assistant outcome
The setup status endpoint now exposes effective environment SMTP defaults, and the setup form populates blank fields from them. Persisted setup values take precedence after saving.
### User
Use OAuth to register with the Mastodon account and obtain access to post.
### Assistant outcome
Added Mastodon OAuth app registration, authorization and callback endpoints, one-time OAuth state storage, and per-user access-token persistence.
### User
Allow the user to enter the Mastodon server to authenticate to.
### Assistant outcome
The profile now requires an explicit Mastodon server, saves that server before OAuth, and authenticates against the selected instance.
### User
Clicking authenticate with this server for social.secret-wg.org generates a 502 error.
### Assistant outcome
Confirmed the Mastodon instance was returning HTTP 429 during app registration. OAuth app credentials are now cached per instance, and upstream rate-limit responses are passed through with retry information instead of being reported as generic 502 errors.
### User
When a password is mistyped, send a password reset link.
### Assistant outcome
Added hashed, expiring, single-use password-reset tokens, SMTP reset mail, a reset-password page, generic login errors, and session revocation after reset.
### User
Log the unlogged chat and prompt in the VIBE directory.
### Assistant outcome
Appended the previously unlogged feature work and this request to `VIBE/PROMPTS.md` and `VIBE/CHAT_LOG.md`.
### User
Continue to log prompts and chats in VIBE
### User
Use the VIBE directory in the current workspace.
### Assistant outcome
Confirmed the current workspace is `/home/olaf/LinkLog_Dev` and appended both requests and this outcome to the VIBE logs located at `VIBE/PROMPTS.md` and `VIBE/CHAT_LOG.md`.
### User
Posting to Mastodon seems to fail, try to fix, add debug logging.
### Assistant outcome
Changed Mastodon status publishing to form-encoded API requests, added safe configurable logging for skipped, successful, and failed posts, included upstream HTTP response details without logging access tokens, and wired `LINKLOG_LOG_LEVEL` through the environment and Compose configuration. Updated the posting regression test. The focused test and complete Dockerized backend suite passed with 22 tests.
### User
`LINKLOG_PUBLIC_URL` and `TRAEFIK_HOST` are the same and can be merged. Use `LINKLOG_PUBLIC_URL` in Docker Compose.
### 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.
+27
View File
@@ -88,6 +88,33 @@
84. Add the version to the name of the resulting xpi 84. Add the version to the name of the resulting xpi
85. Set the version number of the backend to 0.1.0 85. Set the version number of the backend to 0.1.0
84. Add the creation of an xpi bundle for the webextention to the Makefile 84. Add the creation of an xpi bundle for the webextention to the Makefile
85. Correct: A signed XPI will be generated manually and checked into the repository under XPI/signed
86. The "data_collection_permissions" property is missing from the plugin
87. Validate the XPI during the make process of the unsigned xpi
88. pressing remove still does not remove a user.
89. Trying to remove user alice: Could not remove alice: Internal Server Error
90. create a gitea action that is ran every time main is updated and that creates a docker image tagged development
91. Use an access token to publish to registry, not username and password
92. Add SMTP capabilities to the backend. Use it to validate the email addresses using a validation link in mail
93. Do not configure default users at bootstrap. Instead present a configuration page (only present if no administrator is configured). The configuration page asks for the admin users credetials and allows to configure the SMTP settings and sends a test mail after configuration
94. After saving the configuration - add link to home page together with "LinkLog is configured and the SMTP test mail was sent."
95. In the confiuration seperate the safe and send mail functionality. Allow the user to resend test mail 5 times with a 20 seconds interval and then hold back for 2 minutes - show a visual timer counting down.
96. when running the initial config use the defaults from the .env file when available
97. Use oauth to register with the mastodon account and obtain access to post
98. Allow user to enter the mastodon server to authenticate to
99. Clicking authenticate with this server (social.secret-wg.org) generates 502 error
100. Log the unlogged chat and promt in the VIBE directory
101. when password is mistyped send a password reset link
102. when running the initial config use the defaults from the .env file when available
103. After saving the configuration - add link to home page together with "LinkLog is configured and the SMTP test mail was sent."
104. In the confiuration seperate the safe and send mail functionality. Allow the user to resend test mail 5 times with a 20 seconds interval and then hold back for 2 minutes - show a visual timer counting down.
105. Use oauth to register with the mastodon account and obtain access to post
106. Allow user to enter the mastodon server to authenticate to
107. Log the unlogged chat and promt in the VIBE directory
108. Continue to log prompts and chats in VIBE
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
## Future entries ## Future entries
+682583
View File
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -2,6 +2,7 @@
## SPDX-License-Identifier: GPL-3.0-or-later ## SPDX-License-Identifier: GPL-3.0-or-later
from fastapi import APIRouter, Header, HTTPException, status from fastapi import APIRouter, Header, HTTPException, status
import logging
from pydantic import BaseModel from pydantic import BaseModel
from backend.app.services.link_service import create_link, list_public_links, list_tags, update_link from backend.app.services.link_service import create_link, list_public_links, list_tags, update_link
@@ -9,6 +10,7 @@ from backend.app.services.plugin_manager import plugin_manager
from backend.app.services.token_service import validate_token from backend.app.services.token_service import validate_token
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__)
class LinkCreate(BaseModel): class LinkCreate(BaseModel):
@@ -44,7 +46,9 @@ def create_link_endpoint(payload: LinkCreate, authorization: str | None = Header
record = create_link(info['user_id'], payload.title, payload.url, payload.comment, payload.timestamp, payload.tags) record = create_link(info['user_id'], payload.title, payload.url, payload.comment, payload.timestamp, payload.tags)
except ValueError as error: except ValueError as error:
raise HTTPException(status_code=422, detail=str(error)) from error raise HTTPException(status_code=422, detail=str(error)) from error
plugin_manager.dispatch({'type': 'link_created', **record}) plugin_results = plugin_manager.dispatch({'type': 'link_created', **record})
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 return record
+10 -1
View File
@@ -10,6 +10,14 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent
DB_PATH = BASE_DIR / 'data' / 'linklog.db' DB_PATH = BASE_DIR / 'data' / 'linklog.db'
def normalize_public_url(value: str) -> str:
value = value.strip().rstrip('/')
if '://' in value:
return value
scheme = 'http' if value.startswith(('localhost', '127.0.0.1')) else 'https'
return f'{scheme}://{value}'
@dataclass @dataclass
class Settings: class Settings:
app_name: str = os.getenv('LINKLOG_APP_NAME', 'LinkLog') app_name: str = os.getenv('LINKLOG_APP_NAME', 'LinkLog')
@@ -17,7 +25,7 @@ class Settings:
database_url: str = os.getenv('LINKLOG_DATABASE_URL', f'sqlite:///{DB_PATH}') database_url: str = os.getenv('LINKLOG_DATABASE_URL', f'sqlite:///{DB_PATH}')
secret_key: str = os.getenv('LINKLOG_SECRET_KEY', 'dev-secret-key-change-me') secret_key: str = os.getenv('LINKLOG_SECRET_KEY', 'dev-secret-key-change-me')
token_expiry_days: int = int(os.getenv('LINKLOG_TOKEN_EXPIRY_DAYS', '30')) token_expiry_days: int = int(os.getenv('LINKLOG_TOKEN_EXPIRY_DAYS', '30'))
public_url: str = os.getenv('LINKLOG_PUBLIC_URL', 'http://localhost:8000').rstrip('/') public_url: str = normalize_public_url(os.getenv('LINKLOG_PUBLIC_URL', 'http://localhost:8000'))
smtp_host: str = os.getenv('LINKLOG_SMTP_HOST', '') smtp_host: str = os.getenv('LINKLOG_SMTP_HOST', '')
smtp_port: int = int(os.getenv('LINKLOG_SMTP_PORT', '587')) smtp_port: int = int(os.getenv('LINKLOG_SMTP_PORT', '587'))
smtp_username: str = os.getenv('LINKLOG_SMTP_USERNAME', '') smtp_username: str = os.getenv('LINKLOG_SMTP_USERNAME', '')
@@ -28,6 +36,7 @@ class Settings:
password_reset_expiry_hours: int = int(os.getenv('LINKLOG_PASSWORD_RESET_EXPIRY_HOURS', '1')) password_reset_expiry_hours: int = int(os.getenv('LINKLOG_PASSWORD_RESET_EXPIRY_HOURS', '1'))
mastodon_client_name: str = os.getenv('LINKLOG_MASTODON_CLIENT_NAME', 'LinkLog') mastodon_client_name: str = os.getenv('LINKLOG_MASTODON_CLIENT_NAME', 'LinkLog')
mastodon_oauth_expiry_minutes: int = int(os.getenv('LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES', '10')) mastodon_oauth_expiry_minutes: int = int(os.getenv('LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES', '10'))
log_level: str = os.getenv('LINKLOG_LOG_LEVEL', 'INFO').upper()
tracking_params: list[str] = None tracking_params: list[str] = None
def __post_init__(self): def __post_init__(self):
+3
View File
@@ -2,6 +2,7 @@
## SPDX-License-Identifier: GPL-3.0-or-later ## SPDX-License-Identifier: GPL-3.0-or-later
from fastapi import FastAPI from fastapi import FastAPI
import logging
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -20,6 +21,8 @@ from backend.app.core.config import settings
from backend.app.database import AVATARS_DIR from backend.app.database import AVATARS_DIR
from backend.app.services.link_service import list_public_links from backend.app.services.link_service import list_public_links
logging.basicConfig(level=getattr(logging, settings.log_level, logging.INFO))
app = FastAPI(title='LinkLog API', version=settings.version) app = FastAPI(title='LinkLog API', version=settings.version)
app.mount('/static', StaticFiles(directory='frontend/static'), name='static') app.mount('/static', StaticFiles(directory='frontend/static'), name='static')
app.mount('/media', StaticFiles(directory=AVATARS_DIR), name='media') app.mount('/media', StaticFiles(directory=AVATARS_DIR), name='media')
+41 -6
View File
@@ -2,12 +2,15 @@
## SPDX-License-Identifier: GPL-3.0-or-later ## SPDX-License-Identifier: GPL-3.0-or-later
import json import json
import logging
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from backend.app.plugins.base import BasePlugin from backend.app.plugins.base import BasePlugin
DEFAULT_POST_PREFIX = 'From my #LinkLog: ' DEFAULT_POST_PREFIX = 'From my #LinkLog: '
logger = logging.getLogger(__name__)
class DefaultFrontendPlugin(BasePlugin): class DefaultFrontendPlugin(BasePlugin):
@@ -50,6 +53,10 @@ class MastodonPlugin(BasePlugin):
instance = f'https://{instance}' instance = f'https://{instance}'
access_token = str(config.get('access_token', '')).strip() access_token = str(config.get('access_token', '')).strip()
if not instance or not access_token: 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'} return {'status': 'skipped', 'plugin': self.name, 'reason': 'not_configured'}
status_parts = [event.get('title') or event.get('url', '')] status_parts = [event.get('title') or event.get('url', '')]
@@ -63,25 +70,51 @@ class MastodonPlugin(BasePlugin):
if event.get('tags'): if event.get('tags'):
status = f'{status} {" ".join(event["tags"])}' status = f'{status} {" ".join(event["tags"])}'
status_parts.append(status) status_parts.append(status)
post_body = '\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: try:
request = Request( request = Request(
f'{instance}/api/v1/statuses', endpoint,
data=json.dumps({'status': '\n'.join(status_parts)}).encode('utf-8'), data=urlencode({'status': post_body}).encode('utf-8'),
headers={ headers={
'Authorization': f'Bearer {access_token}', 'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'User-Agent': 'LinkLog/1.0',
}, },
method='POST', method='POST',
) )
with urlopen(request, timeout=5) as response: with urlopen(request, timeout=5) as response:
response_data = json.loads(response.read().decode('utf-8')) 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 { return {
'status': 'posted', 'status': 'posted',
'plugin': self.name, 'plugin': self.name,
'post_id': response_data.get('id'), 'post_id': response_data.get('id'),
} }
except (HTTPError, URLError, TimeoutError, OSError, ValueError) as error: 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 { return {
'status': 'failed', 'status': 'failed',
'plugin': self.name, 'plugin': self.name,
@@ -111,7 +144,9 @@ class PluginManager:
results = [] results = []
for plugin in self.plugins: for plugin in self.plugins:
if plugin.enabled: if plugin.enabled:
results.append(plugin.handle_event(event)) 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 return results
+5 -4
View File
@@ -4,6 +4,7 @@
import json import json
import threading import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs
from uuid import uuid4 from uuid import uuid4
from unittest.mock import patch from unittest.mock import patch
@@ -410,7 +411,8 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
def do_POST(self): def do_POST(self):
received['path'] = self.path received['path'] = self.path
received['authorization'] = self.headers['Authorization'] received['authorization'] = self.headers['Authorization']
received['body'] = json.loads(self.rfile.read(int(self.headers['Content-Length']))) received['content_type'] = self.headers['Content-Type']
received['body'] = parse_qs(self.rfile.read(int(self.headers['Content-Length'])).decode())
self.send_response(200) self.send_response(200)
self.send_header('Content-Type', 'application/json') self.send_header('Content-Type', 'application/json')
self.end_headers() self.end_headers()
@@ -442,9 +444,8 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
assert response.status_code == 201 assert response.status_code == 201
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['body'] == { assert received['content_type'] == 'application/x-www-form-urlencoded'
'status': 'A useful page\nWorth sharing\nFrom my #LinkLog: https://example.com/useful #python #web', assert received['body'] == {'status': ['A useful page\nWorth sharing\nFrom my #LinkLog: https://example.com/useful #python #web']}
}
finally: finally:
server.shutdown() server.shutdown()
thread.join() thread.join()
+4 -3
View File
@@ -14,7 +14,7 @@ services:
LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db}
LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env}
LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30} LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30}
LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-https://linklog.example.com} LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-linklog.example.com}
LINKLOG_SMTP_HOST: ${LINKLOG_SMTP_HOST:-smtp.example.com} LINKLOG_SMTP_HOST: ${LINKLOG_SMTP_HOST:-smtp.example.com}
LINKLOG_SMTP_PORT: ${LINKLOG_SMTP_PORT:-587} LINKLOG_SMTP_PORT: ${LINKLOG_SMTP_PORT:-587}
LINKLOG_SMTP_USERNAME: ${LINKLOG_SMTP_USERNAME:?Set LINKLOG_SMTP_USERNAME in .env} LINKLOG_SMTP_USERNAME: ${LINKLOG_SMTP_USERNAME:?Set LINKLOG_SMTP_USERNAME in .env}
@@ -25,6 +25,7 @@ services:
LINKLOG_PASSWORD_RESET_EXPIRY_HOURS: ${LINKLOG_PASSWORD_RESET_EXPIRY_HOURS:-1} LINKLOG_PASSWORD_RESET_EXPIRY_HOURS: ${LINKLOG_PASSWORD_RESET_EXPIRY_HOURS:-1}
LINKLOG_MASTODON_CLIENT_NAME: ${LINKLOG_MASTODON_CLIENT_NAME:-LinkLog} LINKLOG_MASTODON_CLIENT_NAME: ${LINKLOG_MASTODON_CLIENT_NAME:-LinkLog}
LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES: ${LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES:-10} LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES: ${LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES:-10}
LINKLOG_LOG_LEVEL: ${LINKLOG_LOG_LEVEL:-INFO}
LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-} LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-}
restart: ${APP_RESTART_POLICY:-unless-stopped} restart: ${APP_RESTART_POLICY:-unless-stopped}
healthcheck: healthcheck:
@@ -41,10 +42,10 @@ services:
traefik.http.routers.linklog.entrypoints: web traefik.http.routers.linklog.entrypoints: web
traefik.http.routers.linklog.rule: Host(`linklog.example.com`) # Make sure to change traefik.http.routers.linklog.rule: Host(`${LINKLOG_PUBLIC_URL:-linklog.example.com}`) # Make sure to change
traefik.http.routers.linklog.middlewares: web-https-redirect,servicests # these middlewares must exist traefik.http.routers.linklog.middlewares: web-https-redirect,servicests # these middlewares must exist
traefik.http.routers.linklog-secure.entrypoints: websecure traefik.http.routers.linklog-secure.entrypoints: websecure
traefik.http.routers.linklog-secure.rule: Host(`linklog.example.com`) #Make sure to change traefik.http.routers.linklog-secure.rule: Host(`${LINKLOG_PUBLIC_URL:-linklog.example.com}`) #Make sure to change
traefik.http.routers.linklog-secure.tls: true traefik.http.routers.linklog-secure.tls: true
traefik.http.routers.linklog-secure.middlewares: servicests traefik.http.routers.linklog-secure.middlewares: servicests
+32 -16
View File
@@ -10,24 +10,15 @@ services:
ports: ports:
- "${APP_PORT:-8000}:8000" - "${APP_PORT:-8000}:8000"
volumes: volumes:
- linklog_data:/app/backend/data - ./linklog_data:/app/backend/data
environment: environment:
APP_ENV: ${APP_ENV:-production} APP_ENV: ${APP_ENV:-production}
LINKLOG_APP_NAME: ${LINKLOG_APP_NAME:-LinkLog} LINKLOG_APP_NAME: ${LINKLOG_APP_NAME:-LinkLog}
LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db} LINKLOG_DATABASE_PATH: ${LINKLOG_DATABASE_PATH:-/app/backend/data/linklog.db}
LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env} LINKLOG_SECRET_KEY: ${LINKLOG_SECRET_KEY:?Set LINKLOG_SECRET_KEY in .env}
LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-localhost}
LINKLOG_LOG_LEVEL: ${LINKLOG_LOG_LEVEL:-INFO}
LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30} LINKLOG_TOKEN_EXPIRY_DAYS: ${LINKLOG_TOKEN_EXPIRY_DAYS:-30}
LINKLOG_PUBLIC_URL: ${LINKLOG_PUBLIC_URL:-http://localhost:8000}
LINKLOG_SMTP_HOST: ${LINKLOG_SMTP_HOST:-}
LINKLOG_SMTP_PORT: ${LINKLOG_SMTP_PORT:-587}
LINKLOG_SMTP_USERNAME: ${LINKLOG_SMTP_USERNAME:-}
LINKLOG_SMTP_PASSWORD: ${LINKLOG_SMTP_PASSWORD:-}
LINKLOG_SMTP_FROM: ${LINKLOG_SMTP_FROM:-LinkLog <no-reply@localhost>}
LINKLOG_SMTP_USE_TLS: ${LINKLOG_SMTP_USE_TLS:-true}
LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS: ${LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS:-24}
LINKLOG_PASSWORD_RESET_EXPIRY_HOURS: ${LINKLOG_PASSWORD_RESET_EXPIRY_HOURS:-1}
LINKLOG_MASTODON_CLIENT_NAME: ${LINKLOG_MASTODON_CLIENT_NAME:-LinkLog}
LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES: ${LINKLOG_MASTODON_OAUTH_EXPIRY_MINUTES:-10}
LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-} LINKLOG_TRACKING_PARAMS: ${LINKLOG_TRACKING_PARAMS:-}
restart: ${APP_RESTART_POLICY:-unless-stopped} restart: ${APP_RESTART_POLICY:-unless-stopped}
healthcheck: healthcheck:
@@ -36,7 +27,32 @@ services:
timeout: ${APP_HEALTHCHECK_TIMEOUT:-5s} timeout: ${APP_HEALTHCHECK_TIMEOUT:-5s}
start_period: ${APP_HEALTHCHECK_START_PERIOD:-10s} start_period: ${APP_HEALTHCHECK_START_PERIOD:-10s}
retries: ${APP_HEALTHCHECK_RETRIES:-3} retries: ${APP_HEALTHCHECK_RETRIES:-3}
labels:
traefik.enable: true
volumes: traefik.http.middlewares.web-https-redirect.redirectscheme.scheme: https
linklog_data: traefik.http.services.linklog.loadbalancer.server.port: 8000
traefik.docker.network: git_traefik
traefik.http.routers.linklog.entrypoints: web
traefik.http.routers.linklog.rule: Host(`${LINKLOG_PUBLIC_URL:-localhost}`)
traefik.http.routers.linklog.middlewares: web-https-redirect,servicests
traefik.http.routers.linklog-secure.entrypoints: websecure
traefik.http.routers.linklog-secure.rule: Host(`${LINKLOG_PUBLIC_URL:-localhost}`)
traefik.http.routers.linklog-secure.tls: true
traefik.http.routers.linklog-secure.middlewares: servicests
traefik.http.routers.linklog-secure.tls.certresolver: myresolver
traefik.http.routers.linklog-secure.service: linklog
networks:
- linklog_traefik
networks:
linklog_traefik:
external: true
name: linklog_traefik