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_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.
|
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
|
## Useful API Calls
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@@ -17,6 +18,14 @@ router = APIRouter()
|
|||||||
def oauth_start(instance: str = 'mastodon.social', user: dict = Depends(get_current_user)):
|
def oauth_start(instance: str = 'mastodon.social', user: dict = Depends(get_current_user)):
|
||||||
try:
|
try:
|
||||||
authorization_url = start_authorization(user['id'], instance)
|
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:
|
except Exception as error:
|
||||||
raise HTTPException(status_code=502, detail=f'Could not register with Mastodon: {error}') from error
|
raise HTTPException(status_code=502, detail=f'Could not register with Mastodon: {error}') from error
|
||||||
return {'authorization_url': authorization_url}
|
return {'authorization_url': authorization_url}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from hashlib import sha256
|
|||||||
import json
|
import json
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
from urllib.error import HTTPError
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
from uuid import uuid4
|
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:
|
def start_authorization(user_id: str, instance: str) -> str:
|
||||||
instance = normalize_instance(instance)
|
instance = normalize_instance(instance)
|
||||||
redirect_uri = f'{settings.public_url}/api/mastodon/oauth/callback'
|
redirect_uri = f'{settings.public_url}/api/mastodon/oauth/callback'
|
||||||
|
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', {
|
app = post_form(f'{instance}/api/v1/apps', {
|
||||||
'client_name': settings.mastodon_client_name,
|
'client_name': settings.mastodon_client_name,
|
||||||
'redirect_uris': redirect_uri,
|
'redirect_uris': redirect_uri,
|
||||||
'scopes': 'read:accounts write:statuses',
|
'scopes': 'read:accounts write:statuses',
|
||||||
'website': settings.public_url,
|
'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)
|
state = token_urlsafe(32)
|
||||||
expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.mastodon_oauth_expiry_minutes)
|
expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.mastodon_oauth_expiry_minutes)
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ mastodonConnectButton.addEventListener('click', async () => {
|
|||||||
setStatus('#mastodon-status', `Authorizing with ${instance}...`);
|
setStatus('#mastodon-status', `Authorizing with ${instance}...`);
|
||||||
const response = await fetch(`/api/mastodon/oauth/start?instance=${encodeURIComponent(instance)}`, {headers: authHeaders()});
|
const response = await fetch(`/api/mastodon/oauth/start?instance=${encodeURIComponent(instance)}`, {headers: authHeaders()});
|
||||||
const result = await response.json();
|
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);
|
window.location.assign(result.authorization_url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus('#mastodon-status', error.message, true);
|
setStatus('#mastodon-status', error.message, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user