Files
SocialPub/tools/pasture/town/dialects/akkoma.py
T

94 lines
3.7 KiB
Python
Raw Normal View History

"""Akkoma 3.20 (and Pleroma, which shares all of this): accounts made with pleroma_ctl, tokens through the password
grant, objects read from its Postgres database (`akkoma`). Its Linkify never takes @name@host.test for a mention, so
mentions are addressed with Pleroma's `to[]` as well. Status ids are the Create activity's FlakeId in base62."""
import urllib.parse
from core import podman
from dialects.base import Stored
from dialects.mastodon_api import MastodonApi
PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
B62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def flake(uuid):
n = int(uuid.replace("-", ""), 16)
out = ""
while n:
n, r = divmod(n, 62)
out = B62[r] + out
return out or "0"
class Akkoma(MastodonApi):
platform = "akkoma"
caps = MastodonApi.caps | {"react", "quote"}
container = "pasture-akkoma"
ctl = "/opt/akkoma/bin/pleroma_ctl"
db = "akkoma"
def provision(self, accounts):
existing = {r["nickname"] for r in podman.psql(self.db, "select nickname from users where local")}
for a in accounts:
if a.username in existing:
continue
# pleroma_ctl passes its arguments on unquoted: no value may hold a space
podman.exec_(self.container, self.ctl, "user", "new", a.username, f"{a.username}@{self.host}",
"--password", a.password, "--name", a.username, "--assume-yes")
app = self.http.post(self.base + "/api/v1/apps", ok={200}, form={
"client_name": "pasture-town", "redirect_uris": "urn:ietf:wg:oauth:2.0:oob", "scopes": "read write follow"}).json()
sessions = []
for a in accounts:
token = self.http.post(self.base + "/oauth/token", ok={200}, form={
"grant_type": "password", "username": a.username, "password": a.password, "client_id": app["client_id"],
"client_secret": app["client_secret"], "scope": "read write follow"}).json()["access_token"]
sessions.append(self.session_from_token(a, token))
return sessions
def status_form(self, s, spec):
form = super().status_form(s, spec)
if spec.mentions:
# Pleroma's to[] takes a local user by bare nickname: name@own-host addresses nobody
form["to"] = [a.split("@")[0] if a.endswith("@" + self.host) else a for a in spec.mentions]
return form
def react(self, s, uri, emoji):
sid = self.local_status_id(s, uri) or self.resolve(s, uri)
self.api(s, "PUT", f"/api/v1/pleroma/statuses/{sid}/reactions/{urllib.parse.quote(emoji)}", ok={200})
def _rows(self, uris):
if not uris:
return {}
rows = podman.psql(self.db, """
select o.data->>'id' as uri, o.data as data, a.id::text as act
from objects o
left join activities a on a.data->>'type' = 'Create'
and coalesce(a.data->'object'->>'id', a.data->>'object') = o.data->>'id'
where o.data->>'id' = any(string_to_array(:'p1', ' '))""", " ".join(uris))
out = {}
for r in rows:
d = r["data"]
deleted = d.get("type") == "Tombstone"
to, cc = _list(d.get("to")), _list(d.get("cc"))
if PUBLIC in to:
vis = "public"
elif PUBLIC in cc:
vis = "unlisted"
elif any(x.endswith("/followers") for x in to + cc):
vis = "followers"
else:
vis = "direct"
options = d.get("oneOf") or d.get("anyOf")
votes = [((o.get("replies") or {}).get("totalItems") or 0) for o in options] if options else None
reactions = {e[0]: len(e[1]) for e in d.get("reactions") or [] if len(e) >= 2}
out[r["uri"]] = Stored(True, deleted, flake(r["act"]) if r["act"] else None, vis, d.get("content"),
d.get("summary") or None, bool(d.get("formerRepresentations")), d.get("inReplyTo"),
d.get("like_count") or 0, d.get("announcement_count") or 0, d.get("repliesCount") or 0,
votes, reactions, d)
return out
def _list(v):
if v is None:
return []
return v if isinstance(v, list) else [v]