cli/awcli
Arija A. c9a6f432e6
Add support for new ari-web mblog API
Signed-off-by: Arija A. <ari@ari.lt>
2025-10-28 15:57:47 +02:00

269 lines
7.1 KiB
Python
Executable file

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Ari-web CLI"""
import json
import os
import sys
from typing import Any, Dict, Final, FrozenSet, Optional, Tuple, Union
import click
import requests # type: ignore
ARI_WEB: Final[Optional[str]] = os.getenv("_ARI_WEB")
ARI_WEB_ADMIN_KEY: Final[Optional[str]] = os.getenv("_ARI_WEB_ADMIN_KEY")
MBLOG_INIT_REACTIONS: Final[Tuple[str, ...]] = (
"\U0001f525", # fire
"\U0001f90e", # brown heart
"\U0001f44d", # thumbs up
"\U0001f44e", # thumbs down
"\U0001f62d", # sob
"\U0001f972", # face with one tear
)
if not ARI_WEB or not ARI_WEB_ADMIN_KEY:
click.echo(
"Error: _ARI_WEB and _ARI_WEB_ADMIN_KEY environment variables must be set.",
err=True,
)
sys.exit(1)
HEADERS_MD: Final[Dict[str, str]] = {
"X-Admin-Key": ARI_WEB_ADMIN_KEY,
"Content-Type": "text/markdown",
}
HEADERS_PLAIN: Final[Dict[str, str]] = {
"X-Admin-Key": ARI_WEB_ADMIN_KEY,
"Content-Type": "text/plain",
}
HEADERS_NO_TYPE: Final[Dict[str, str]] = {
"X-Admin-Key": ARI_WEB_ADMIN_KEY,
}
def pretty_print_json(json_data: Dict[str, object]) -> None:
"""Pretty print JSON"""
# Calculate max key length for padding
max_key_len: int = max(len(str(k)) for k in json_data.keys()) if json_data else 0
for key, value in json_data.items():
click.echo(f"{str(key):<{max_key_len}} : {value}")
def print_response(response: requests.Response) -> None:
"""Print response"""
try:
json_data = response.json()
if isinstance(json_data, dict):
pretty_print_json(json_data)
else:
# Not a dict, print raw JSON text
click.echo(json.dumps(json_data, indent=2))
except (json.JSONDecodeError, ValueError):
# Not JSON, fallback to plain text
click.echo(response.text)
def post_markdown(endpoint: str, markdown: str) -> None:
"""POST markdown"""
response = requests.post(f"{ARI_WEB}/{endpoint}", data=markdown, headers=HEADERS_MD)
print_response(response)
def post_form(
endpoint: str,
data: Union[Dict[str, Any], Tuple[Tuple[str, Any], ...]],
files: Optional[dict] = None,
) -> None:
"""POST a form"""
response = requests.post(
f"{ARI_WEB}/{endpoint}",
headers={"X-Admin-Key": ARI_WEB_ADMIN_KEY},
data=data,
files=files,
)
print_response(response)
def post_plain(endpoint: str, content: str) -> None:
"""POST plain text"""
response = requests.post(
f"{ARI_WEB}/{endpoint}", data=content, headers=HEADERS_PLAIN
)
print_response(response)
def delete_resource(endpoint: str, resource_id: str, headers: dict[str, str]) -> None:
"""Do a DELETE request"""
response = requests.delete(f"{ARI_WEB}/{endpoint}/{resource_id}", headers=headers)
print_response(response)
@click.group()
def cli() -> None:
"""Ari-web CLI"""
@cli.group()
def status() -> None:
"""Manage status posts."""
@status.command("post")
@click.argument("content", type=str)
def status_post(content: str) -> None:
"""Post a status update with markdown content."""
post_markdown("status", content)
@cli.group()
def now() -> None:
"""Manage 'now' posts."""
@now.command("post")
@click.argument("content", type=str)
def now_post(content: str) -> None:
"""Post a 'now' status with markdown content."""
post_markdown("now", content)
@now.command("delete")
@click.argument("id_", type=str)
def now_delete(id_: str) -> None:
"""Delete a 'now' post by ID."""
delete_resource("now", id_, HEADERS_MD)
@cli.group()
def pics() -> None:
"""Manage pictures."""
@pics.command("post")
@click.argument("filepath", type=click.Path(exists=True))
@click.option("-a", "--alt", required=True, type=str, help="Alt text for picture")
@click.option("-t", "--title", required=True, type=str, help="Title for picture")
@click.option("-s", "--scale", type=str, help="Scale factor for picture")
@click.option("-u", "--unlisted", is_flag=True, help="Mark picture as unlisted")
def pics_post(
filepath: str, alt: str, title: str, scale: Optional[str], unlisted: bool
) -> None:
"""Add a picture with file, alt text, and title."""
with open(filepath, "rb") as file:
files = {"file": file}
data: dict[str, str] = {"alt": alt, "title": title}
if scale:
data["scale"] = scale
if unlisted:
data["unlisted"] = "1"
post_form("pics", data, files)
@pics.command("delete")
@click.argument("id_", type=str)
def pics_delete(id_: str) -> None:
"""Delete picture by ID."""
delete_resource("pics", id_, HEADERS_NO_TYPE)
@cli.group()
def rss() -> None:
"""Manage RSS feed posts."""
@rss.command("post")
@click.argument("title", type=str)
@click.option("--description", "-d", type=str, help="Description for RSS item")
@click.option("--link", "-l", type=str, help="Link for RSS item")
def rss_post(title: str, description: Optional[str], link: Optional[str]) -> None:
"""Post an RSS item with title and optional description and link."""
data: dict[str, str] = {"title": title}
if description:
data["description"] = description
if link:
data["link"] = link
post_form("rss", data)
@rss.command("delete")
@click.argument("id_", type=str)
def rss_delete(id_: str) -> None:
"""Delete RSS item by ID."""
delete_resource("rss", id_, HEADERS_PLAIN)
@cli.group()
def mblog() -> None:
"""Manage microblog posts."""
@mblog.command("post")
@click.argument("content", type=str)
@click.option(
"--reactions",
"-r",
type=str,
help="Comma-seperated reactions to auto-add",
default=",".join(MBLOG_INIT_REACTIONS),
)
def mblog_post(
content: str,
reactions: str,
) -> None:
"""Post a microblog entry with markdown content."""
emojis: FrozenSet[str] = frozenset(map(str.strip, reactions.split(",")))
post_form(
"mblog",
(
("content", content),
*(("reactions", emoji) for emoji in emojis),
),
)
@mblog.command("delete")
@click.argument("id_", type=str)
def mblog_delete(id_: str) -> None:
"""Delete microblog post by ID."""
delete_resource("mblog", id_, HEADERS_NO_TYPE)
@mblog.command("unreact")
@click.argument("id_", type=str)
@click.option(
"-r", "--reaction", type=str, required=True, help="Emoji reaction to remove"
)
def mblog_unreact(id_: str, reaction: str) -> None:
"""Remove a reaction emoji from a microblog post."""
post_plain(f"mblog/unreact/{id_}", reaction)
@mblog.command("unreactall")
@click.argument("id_", type=str)
def mblog_unreactall(id_: str) -> None:
"""Remove all reaction emojis from a microblog post."""
post_plain(f"mblog/unreactall/{id_}", "")
@mblog.command("togglereact")
@click.argument("id_", type=str)
def mblog_togglereact(id_: str) -> None:
"""Toggle reaction feature of posts."""
post_plain(f"mblog/togglereact/{id_}", "")
@mblog.command("togglepin")
@click.argument("id_", type=str)
def mblog_togglepin(id_: str) -> None:
"""Toggle post pin status."""
post_plain(f"mblog/togglepin/{id_}", "")
if __name__ == "__main__":
cli(prog_name=sys.argv[0])