This commit is contained in:
@@ -233,6 +233,8 @@ After logging in, open <http://localhost:8000/profile>, enter the Mastodon insta
|
||||
|
||||
`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 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.
|
||||
|
||||
Enable the plugin from the admin API or the admin page. New links are saved first and then posted to the configured instance at `/api/v1/statuses`. A Mastodon network failure does not undo the saved link.
|
||||
|
||||
## Useful API Calls
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from urllib.parse import quote
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -17,6 +18,14 @@ router = APIRouter()
|
||||
def oauth_start(instance: str = 'mastodon.social', user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
authorization_url = start_authorization(user['id'], instance)
|
||||
except HTTPError as error:
|
||||
retry_after = error.headers.get('Retry-After') if error.headers else None
|
||||
headers = {'Retry-After': retry_after} if retry_after else None
|
||||
raise HTTPException(
|
||||
status_code=error.code,
|
||||
detail=f'Mastodon returned HTTP {error.code} while registering LinkLog. Try again later.',
|
||||
headers=headers,
|
||||
) from error
|
||||
except Exception as error:
|
||||
raise HTTPException(status_code=502, detail=f'Could not register with Mastodon: {error}') from error
|
||||
return {'authorization_url': authorization_url}
|
||||
|
||||
@@ -6,6 +6,7 @@ from hashlib import sha256
|
||||
import json
|
||||
from secrets import token_urlsafe
|
||||
from urllib.parse import urlencode
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import Request, urlopen
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -34,12 +35,24 @@ def post_form(url: str, values: dict) -> dict:
|
||||
def start_authorization(user_id: str, instance: str) -> str:
|
||||
instance = normalize_instance(instance)
|
||||
redirect_uri = f'{settings.public_url}/api/mastodon/oauth/callback'
|
||||
app = post_form(f'{instance}/api/v1/apps', {
|
||||
'client_name': settings.mastodon_client_name,
|
||||
'redirect_uris': redirect_uri,
|
||||
'scopes': 'read:accounts write:statuses',
|
||||
'website': settings.public_url,
|
||||
})
|
||||
setting_name = f'mastodon_app:{instance}'
|
||||
with get_connection() as conn:
|
||||
row = conn.execute('SELECT value FROM app_settings WHERE name = ?', (setting_name,)).fetchone()
|
||||
app = json.loads(row['value']) if row else None
|
||||
if not app:
|
||||
app = post_form(f'{instance}/api/v1/apps', {
|
||||
'client_name': settings.mastodon_client_name,
|
||||
'redirect_uris': redirect_uri,
|
||||
'scopes': 'read:accounts write:statuses',
|
||||
'website': settings.public_url,
|
||||
})
|
||||
with get_connection() as conn:
|
||||
conn.execute(
|
||||
'''INSERT INTO app_settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP''',
|
||||
(setting_name, json.dumps(app)),
|
||||
)
|
||||
conn.commit()
|
||||
state = token_urlsafe(32)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.mastodon_oauth_expiry_minutes)
|
||||
with get_connection() as conn:
|
||||
|
||||
@@ -111,7 +111,7 @@ mastodonConnectButton.addEventListener('click', async () => {
|
||||
setStatus('#mastodon-status', `Authorizing with ${instance}...`);
|
||||
const response = await fetch(`/api/mastodon/oauth/start?instance=${encodeURIComponent(instance)}`, {headers: authHeaders()});
|
||||
const result = await response.json();
|
||||
if (!response.ok || !result.authorization_url) throw new Error(result.error || 'Could not start Mastodon authorization.');
|
||||
if (!response.ok || !result.authorization_url) throw new Error(result.detail || result.error || 'Could not start Mastodon authorization.');
|
||||
window.location.assign(result.authorization_url);
|
||||
} catch (error) {
|
||||
setStatus('#mastodon-status', error.message, true);
|
||||
|
||||
Reference in New Issue
Block a user