beeper-skill/scripts/beeper.py

185 lines
6.1 KiB
Python
Executable file

#!/usr/bin/env python3
"""Beeper Desktop API helper.
Reads BEEPER_BASE_URL and BEEPER_ACCESS_TOKEN from config.json next to this
file (or from the environment, which takes precedence). Uses only the stdlib.
Usage:
beeper.py info # server info incl. current base URL
beeper.py accounts # list connected accounts/networks
beeper.py chats search QUERY [--type single|group] [--limit N]
beeper.py chats list [--limit N] # recent chats across all accounts
beeper.py messages CHAT_ID [--limit N] [--after-cursor C]
beeper.py send CHAT_ID "text" # send a plain-text message
beeper.py search QUERY [--limit N] # global message search
"""
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.json"
def load_config():
cfg = {}
if CONFIG_PATH.exists():
cfg = json.loads(CONFIG_PATH.read_text())
base = os.environ.get("BEEPER_BASE_URL") or cfg.get("BEEPER_BASE_URL")
token = os.environ.get("BEEPER_ACCESS_TOKEN") or cfg.get("BEEPER_ACCESS_TOKEN")
if not base or not token:
sys.exit("error: BEEPER_BASE_URL and BEEPER_ACCESS_TOKEN required (config.json or env)")
return base.rstrip("/"), token
def api(base, token, method, path, params=None, body=None, timeout=30):
url = f"{base}{path}"
if params:
qs = urllib.parse.urlencode(params)
url = f"{url}?{qs}"
data = None
headers = {"Authorization": f"Bearer {token}"}
if body is not None:
data = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
return resp.status, json.loads(raw) if raw else None
except urllib.error.HTTPError as e:
return e.code, e.read().decode(errors="replace")
def pretty(obj):
return json.dumps(obj, indent=2, ensure_ascii=False)
def quote_chat_id(chat_id):
return urllib.parse.quote(chat_id, safe="")
def cmd_info(base, token, args):
code, data = api(base, token, "GET", "/v1/info")
print(f"HTTP {code}")
print(pretty(data))
def cmd_accounts(base, token, args):
code, data = api(base, token, "GET", "/v1/accounts")
print(f"HTTP {code}")
if not data:
return
for acc in data:
print(f"- {acc.get('network')!r} accountID={acc.get('accountID')} "
f"user={acc.get('user', {}).get('fullName')} "
f"status={acc.get('status')}")
def cmd_chats_search(base, token, args):
params = {"query": args.query, "limit": args.limit or 20}
if args.type:
params["type"] = args.type
code, data = api(base, token, "GET", "/v1/chats/search", params)
print(f"HTTP {code}")
for it in (data or {}).get("items", []):
print(f"id: {it.get('id')}")
print(f" {it.get('network')} | type={it.get('type')} | title={it.get('title')}")
for p in it.get("participants", {}).get("items", []):
print(f" - {p.get('fullName')} {p.get('phoneNumber') or ''}"
+ (" [self]" if p.get("isSelf") else ""))
print()
def cmd_chats_list(base, token, args):
code, data = api(base, token, "GET", "/v1/chats", {"limit": args.limit or 20})
print(f"HTTP {code}")
for it in (data or {}).get("items", []):
print(f"id: {it.get('id')}")
print(f" {it.get('network')} | type={it.get('type')} | title={it.get('title')}")
print()
def cmd_messages(base, token, args):
params = {"limit": args.limit or 50}
if args.after_cursor:
params["cursor"] = args.after_cursor
code, data = api(base, token, "GET",
f"/v1/chats/{quote_chat_id(args.chat_id)}/messages", params)
print(f"HTTP {code}")
if not data:
return
items = data.get("items", [])
for m in sorted(items, key=lambda x: x.get("timestamp") or ""):
print(f"[{m.get('timestamp')}] {m.get('senderName')}: {m.get('text')}")
print(f"\n{len(items)} messages | hasMore={data.get('hasMore')} "
f"| oldestCursor={data.get('oldestCursor')}")
def cmd_send(base, token, args):
code, data = api(base, token, "POST",
f"/v1/chats/{quote_chat_id(args.chat_id)}/messages",
body={"text": args.text})
print(f"HTTP {code}")
if data:
print(pretty(data))
def cmd_search(base, token, args):
params = {"query": args.query, "limit": args.limit or 20}
code, data = api(base, token, "GET", "/v1/messages/search", params)
print(f"HTTP {code}")
for it in (data or {}).get("items", []):
print(f"[{it.get('timestamp')}] {it.get('chatID')}")
print(f" {it.get('senderName')}: {it.get('text')}")
print()
def main():
base, token = load_config()
p = argparse.ArgumentParser(description="Beeper Desktop API helper")
sub = p.add_subparsers(dest="cmd", required=True)
pi = sub.add_parser("info")
pi.set_defaults(fn=cmd_info)
pa = sub.add_parser("accounts")
pa.set_defaults(fn=cmd_accounts)
s = sub.add_parser("chats")
ssub = s.add_subparsers(dest="sub", required=True)
ps = ssub.add_parser("search")
ps.add_argument("query")
ps.add_argument("--type", choices=["single", "group"])
ps.add_argument("--limit", type=int)
ps.set_defaults(fn=cmd_chats_search)
pl = ssub.add_parser("list")
pl.add_argument("--limit", type=int)
pl.set_defaults(fn=cmd_chats_list)
pm = sub.add_parser("messages")
pm.add_argument("chat_id")
pm.add_argument("--limit", type=int)
pm.add_argument("--after-cursor", dest="after_cursor")
pm.set_defaults(fn=cmd_messages)
ps2 = sub.add_parser("send")
ps2.add_argument("chat_id")
ps2.add_argument("text")
ps2.set_defaults(fn=cmd_send)
pg = sub.add_parser("search")
pg.add_argument("query")
pg.add_argument("--limit", type=int)
pg.set_defaults(fn=cmd_search)
args = p.parse_args()
args.fn(base, token, args)
if __name__ == "__main__":
main()