94 lines
3.7 KiB
Python
94 lines
3.7 KiB
Python
"""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]
|