|
from textwrap import dedent |
|
import psycopg2 |
|
import sys |
|
import json |
|
|
|
|
|
def parse(note): |
|
note = note.lstrip() |
|
if not note.startswith('---\r\n'): |
|
return |
|
note = note[5:] |
|
|
|
metadata = list() |
|
|
|
while True: |
|
line, _, note = note.partition('\r\n') |
|
line = line.strip() |
|
if line == '': |
|
continue |
|
if line in ('---', '...'): |
|
# we're done here |
|
break |
|
|
|
key, sep, value = line.partition(':') |
|
|
|
key = key.strip() |
|
value = value.strip() |
|
|
|
if not sep == ':' or key == '' or value == '': |
|
# badly formatted metadata, bail |
|
return |
|
|
|
if key[0] == key[-1] and key[0] in ('"', "'"): |
|
key = key[1:-1] |
|
if value[0] == value[-1] and value[0] in ('"', "'"): |
|
value = value[1:-1] |
|
|
|
if len(key) > 255 or len(value) > 255: |
|
# mastodon limits to 255 chars, bail |
|
# better to let the user fix it themself than |
|
# lose some of their data |
|
return |
|
|
|
metadata.append((key, value)) |
|
|
|
if len(metadata) > 4: |
|
# mastodon only supports 4 fields, bail |
|
return |
|
|
|
return (tuple(metadata), note.strip('\r\n')) |
|
|
|
|
|
def test_parse_empty(): |
|
assert parse("hello") is None |
|
|
|
|
|
def test_parse(): |
|
assert parse(dedent("""---\r\ncomputer: 'over'\r\nvirus: very yes 👾\r\n...\r\n\r\ni'm fuckass and I walk like this. im the big boss here i run this town\r\n""" |
|
)) == ( |
|
( |
|
("computer","over"), |
|
("virus", "very yes 👾") |
|
), |
|
"i'm fuckass and I walk like this. im the big boss here i run this town") |
|
|
|
def test_parse_incorrect(): |
|
assert parse(dedent("""---\r\ncomputer:\r\n...\r\n\r\ni'm fuckass and I walk like this. im the big boss here i run this town\r\n""" |
|
)) is None |
|
|
|
|
|
def test_parse_dupe(): |
|
assert parse(dedent("""---\r\nvirus: no\r\nvirus: yes\r\n---\r\n""" |
|
)) == ( |
|
( |
|
("virus", "no"), |
|
("virus", "yes") |
|
), |
|
"" |
|
) |
|
|
|
def to_json(meta): |
|
out = list() |
|
for row in meta: |
|
key, value = row |
|
out.append({"name": key, "value": value}) |
|
return json.dumps(out) |
|
|
|
|
|
def usage(): |
|
print(dedent(""" |
|
usage: |
|
python converter.py DBNAME [POSTGRES CONNECTION STRING] |
|
|
|
your DBNAME is probably mastodon_production |
|
|
|
if you followed the production guide then you don't need a connection string |
|
|
|
more info on connection strings at |
|
<https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING> |
|
|
|
this will ask you to confirm every change before applying any of it |
|
|
|
nonetheless, back up your database before doing this. im not responsible if your instance breaks |
|
|
|
thanks. report issues to codl@codl.fr |
|
""")) |
|
|
|
|
|
if __name__ == '__main__': |
|
if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'): |
|
usage() |
|
exit(128) |
|
|
|
pg = psycopg2.connect(" ".join(sys.argv[3:]), dbname=sys.argv[1]) |
|
|
|
cur = pg.cursor() |
|
|
|
cur.execute('SELECT accounts.id, accounts.username, accounts.note' + |
|
' FROM accounts, users WHERE users.account_id = accounts.id;') |
|
|
|
to_update = list() |
|
|
|
for row in cur: |
|
account_id, username, note = row |
|
parsed = parse(note) |
|
if not parsed or len(parsed[0]) == 0: |
|
print('@{} did not have valid metadata'.format(username)) |
|
continue |
|
to_update.append((account_id, username, note) + parsed) |
|
|
|
for row in to_update: |
|
account_id, username, oldnote, meta, note = row |
|
print(dedent(""" |
|
|
|
|
|
We will update @{username} (#{account_id}) |
|
Original bio was: |
|
{oldnote} |
|
|
|
Found metadata: |
|
{meta} |
|
|
|
Stripped bio: |
|
{note} |
|
""").format(username=username, oldnote=oldnote, meta=meta, |
|
note=note, account_id=account_id)) |
|
response = input("Is this okay? (y/N) ") |
|
if not response or not response[0] in 'yY': |
|
print("Okay. Aborting.") |
|
exit(1) |
|
|
|
cur.execute( |
|
'UPDATE accounts SET note = %s, fields = %s WHERE id = %s', |
|
(note, to_json(meta), account_id)) |
|
|
|
pg.commit() |
|
print("Done!") |