162 lines
6.9 KiB
Python
162 lines
6.9 KiB
Python
## Copyright © 2026 Olaf Kolkman
|
|
## SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import json
|
|
from fastapi import APIRouter, Header, HTTPException, Response, status
|
|
import logging
|
|
from pydantic import BaseModel
|
|
|
|
from backend.app.services.link_service import create_link, delete_link, find_owned_link_by_title_url, get_link_tags, get_owned_link, list_public_links, list_tags, mark_mastodon_posted, update_link
|
|
from backend.app.database import get_connection
|
|
from backend.app.services.plugin_manager import plugin_manager
|
|
from backend.app.services.token_service import validate_token
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LinkCreate(BaseModel):
|
|
title: str
|
|
url: str
|
|
comment: str = ''
|
|
timestamp: str | None = None
|
|
tags: list[str] = []
|
|
|
|
|
|
class LinkUpdate(BaseModel):
|
|
title: str
|
|
url: str
|
|
comment: str = ''
|
|
tags: list[str] = []
|
|
|
|
|
|
@router.get('/tags')
|
|
def available_tags():
|
|
return list_tags()
|
|
|
|
|
|
@router.get('/links/check')
|
|
def check_existing_link(
|
|
title: str,
|
|
url: str,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
if not authorization or not authorization.startswith('Bearer '):
|
|
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
|
info = validate_token(authorization.replace('Bearer ', '', 1))
|
|
if info is None:
|
|
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
|
record = find_owned_link_by_title_url(info['user_id'], title, url)
|
|
return {'exists': record is not None}
|
|
|
|
|
|
@router.post('/links', status_code=status.HTTP_201_CREATED)
|
|
def create_link_endpoint(payload: LinkCreate, response: Response, authorization: str | None = Header(default=None)):
|
|
if not authorization or not authorization.startswith('Bearer '):
|
|
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
|
token = authorization.replace('Bearer ', '', 1)
|
|
info = validate_token(token)
|
|
if info is None:
|
|
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
|
|
|
try:
|
|
record = find_owned_link_by_title_url(info['user_id'], payload.title, payload.url)
|
|
duplicate = record is not None
|
|
if duplicate:
|
|
record = update_link(record['id'], info['user_id'], payload.title, payload.url, payload.comment, payload.tags)
|
|
else:
|
|
record = create_link(info['user_id'], payload.title, payload.url, payload.comment, payload.timestamp, payload.tags)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=422, detail=str(error)) from error
|
|
if duplicate:
|
|
response.status_code = status.HTTP_200_OK
|
|
plugin_results = plugin_manager.dispatch({'type': 'link_created', **record})
|
|
mastodon_result = next((result for result in plugin_results if result.get('plugin') == 'mastodon'), None)
|
|
if mastodon_result and mastodon_result.get('status') == 'posted':
|
|
mark_mastodon_posted(record['id'], info['user_id'], mastodon_result.get('post_id'))
|
|
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)
|
|
plugin_errors = [
|
|
{'plugin': result.get('plugin', 'unknown'), 'reason': result.get('reason', 'Plugin failed')}
|
|
for result in plugin_results
|
|
if result.get('status') == 'failed'
|
|
]
|
|
return {**record, 'duplicate': duplicate, 'plugin_errors': plugin_errors}
|
|
|
|
|
|
@router.put('/links/{link_id}')
|
|
def update_link_endpoint(
|
|
link_id: str,
|
|
payload: LinkUpdate,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
if not authorization or not authorization.startswith('Bearer '):
|
|
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
|
info = validate_token(authorization.replace('Bearer ', '', 1))
|
|
if info is None:
|
|
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
|
|
|
try:
|
|
record = update_link(link_id, info['user_id'], payload.title, payload.url, payload.comment, payload.tags)
|
|
except ValueError as error:
|
|
raise HTTPException(status_code=422, detail=str(error)) from error
|
|
if record is None:
|
|
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
|
return record
|
|
|
|
|
|
@router.delete('/links/{link_id}')
|
|
def delete_link_endpoint(
|
|
link_id: str,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
if not authorization or not authorization.startswith('Bearer '):
|
|
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
|
info = validate_token(authorization.replace('Bearer ', '', 1))
|
|
if info is None:
|
|
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
|
link = get_owned_link(link_id, info['user_id'])
|
|
if link is None:
|
|
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
|
post_ids = json.loads(link['mastodon_post_ids']) if link.get('mastodon_post_ids') else []
|
|
if not post_ids and link.get('mastodon_post_id'):
|
|
post_ids = [link['mastodon_post_id']]
|
|
if post_ids:
|
|
result = plugin_manager.delete_mastodon_posts({**link, 'mastodon_post_ids': post_ids})
|
|
if result.get('status') != 'deleted':
|
|
raise HTTPException(status_code=502, detail=result.get('reason', 'Could not delete Mastodon posts'))
|
|
if not delete_link(link_id, info['user_id']):
|
|
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
|
return {'status': 'deleted', 'id': link_id}
|
|
|
|
|
|
@router.post('/links/{link_id}/mastodon')
|
|
def post_link_to_mastodon(
|
|
link_id: str,
|
|
authorization: str | None = Header(default=None),
|
|
):
|
|
if not authorization or not authorization.startswith('Bearer '):
|
|
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
|
info = validate_token(authorization.replace('Bearer ', '', 1))
|
|
if info is None:
|
|
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
|
with get_connection() as conn:
|
|
row = conn.execute('SELECT * FROM links WHERE id = ? AND user_id = ?', (link_id, info['user_id'])).fetchone()
|
|
if row is None:
|
|
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
|
event = dict(row)
|
|
with get_connection() as conn:
|
|
event['tags'] = get_link_tags(conn, link_id)
|
|
result = plugin_manager.post_to_mastodon({'type': 'link_created', **event})
|
|
if result.get('status') == 'posted':
|
|
mark_mastodon_posted(link_id, info['user_id'], result.get('post_id'))
|
|
return {'status': 'posted', 'post_id': result.get('post_id')}
|
|
if result.get('status') == 'skipped':
|
|
raise HTTPException(status_code=409, detail='Mastodon is not enabled or configured')
|
|
raise HTTPException(status_code=502, detail=result.get('reason', 'Mastodon post failed'))
|
|
|
|
|
|
@router.get('/links')
|
|
def list_links():
|
|
return list_public_links()
|