269 lines
7.1 KiB
Python
Executable file
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])
|