This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
## Copyright © 2026 Olaf Kolkman
|
||||
## SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from secrets import token_urlsafe
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request, urlopen
|
||||
from uuid import uuid4
|
||||
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.database import get_connection
|
||||
|
||||
|
||||
def normalize_instance(instance: str) -> str:
|
||||
value = instance.strip().rstrip('/')
|
||||
if not value:
|
||||
raise ValueError('Mastodon instance is required')
|
||||
return value if '://' in value else f'https://{value}'
|
||||
|
||||
|
||||
def post_form(url: str, values: dict) -> dict:
|
||||
request = Request(
|
||||
url,
|
||||
data=urlencode(values).encode('utf-8'),
|
||||
headers={'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
method='POST',
|
||||
)
|
||||
with urlopen(request, timeout=10) as response:
|
||||
return json.loads(response.read().decode('utf-8'))
|
||||
|
||||
|
||||
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,
|
||||
})
|
||||
state = token_urlsafe(32)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.mastodon_oauth_expiry_minutes)
|
||||
with get_connection() as conn:
|
||||
conn.execute('DELETE FROM mastodon_oauth_states WHERE user_id = ?', (user_id,))
|
||||
conn.execute(
|
||||
'''INSERT INTO mastodon_oauth_states
|
||||
(id, user_id, state_hash, instance, client_id, client_secret, redirect_uri, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||
(str(uuid4()), user_id, sha256(state.encode()).hexdigest(), instance,
|
||||
app['client_id'], app['client_secret'], redirect_uri, expires_at.isoformat()),
|
||||
)
|
||||
conn.commit()
|
||||
return f'{instance}/oauth/authorize?' + urlencode({
|
||||
'client_id': app['client_id'],
|
||||
'redirect_uri': redirect_uri,
|
||||
'response_type': 'code',
|
||||
'scope': 'read:accounts write:statuses',
|
||||
'state': state,
|
||||
})
|
||||
|
||||
|
||||
def finish_authorization(code: str, state: str) -> str:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
with get_connection() as conn:
|
||||
record = conn.execute(
|
||||
'''SELECT * FROM mastodon_oauth_states
|
||||
WHERE state_hash = ? AND expires_at > ?''',
|
||||
(sha256(state.encode()).hexdigest(), now),
|
||||
).fetchone()
|
||||
if record is None:
|
||||
raise ValueError('OAuth state is invalid or expired')
|
||||
conn.execute('DELETE FROM mastodon_oauth_states WHERE id = ?', (record['id'],))
|
||||
conn.commit()
|
||||
token = post_form(f"{record['instance']}/oauth/token", {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'client_id': record['client_id'],
|
||||
'client_secret': record['client_secret'],
|
||||
'redirect_uri': record['redirect_uri'],
|
||||
})
|
||||
access_token = token.get('access_token')
|
||||
if not access_token:
|
||||
raise ValueError('Mastodon did not return an access token')
|
||||
with get_connection() as conn:
|
||||
current = conn.execute(
|
||||
'SELECT config FROM user_plugin_config WHERE user_id = ? AND plugin_name = ?',
|
||||
(record['user_id'], 'mastodon'),
|
||||
).fetchone()
|
||||
config = json.loads(current['config']) if current and current['config'] else {}
|
||||
config.update({'instance': record['instance'], 'access_token': access_token})
|
||||
if current:
|
||||
conn.execute(
|
||||
'UPDATE user_plugin_config SET config = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND plugin_name = ?',
|
||||
(json.dumps(config), record['user_id'], 'mastodon'),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
'''INSERT INTO user_plugin_config
|
||||
(id, user_id, plugin_name, config, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)''',
|
||||
(str(uuid4()), record['user_id'], 'mastodon', json.dumps(config)),
|
||||
)
|
||||
conn.commit()
|
||||
return record['user_id']
|
||||
Reference in New Issue
Block a user