From d32d5877d22f748c1f5ef1ae1ca79bd879d6935a Mon Sep 17 00:00:00 2001 From: Andrea Cornell Date: Tue, 1 Nov 2022 21:05:49 -0400 Subject: [PATCH] joinbot build/packaging --- joinbot/.gitignore | 2 + {live-autojoin => joinbot}/README.md | 0 joinbot/deploy | 7 + joinbot/joinbot/announce.py | 59 ++++++ joinbot/joinbot/background.py | 182 ++++++++++++++++++ {live-autojoin => joinbot/joinbot}/common.py | 0 joinbot/joinbot/service.py | 161 ++++++++++++++++ .../web.py => joinbot/joinbot/web/__init__.py | 2 +- .../joinbot/web}/templates/error.html | 0 .../joinbot/web}/templates/status.html | 0 joinbot/pyproject.toml | 3 + joinbot/setup.cfg | 22 +++ joinbot/setup.py | 3 + live-autojoin/announce.py | 58 ------ live-autojoin/background.py | 180 ----------------- live-autojoin/deploy | 5 - live-autojoin/service.py | 154 --------------- .../systemd/live-autojoin-announce.service | 8 - .../systemd/live-autojoin-cron.service | 7 - .../systemd/live-autojoin-cron.timer | 3 - .../systemd/live-autojoin-service.service | 8 - .../systemd/live-autojoin-web.service | 7 - .../systemd/live-autojoin-web.socket | 4 - live-autojoin/systemd/live-autojoin.target | 5 - 24 files changed, 440 insertions(+), 440 deletions(-) create mode 100644 joinbot/.gitignore rename {live-autojoin => joinbot}/README.md (100%) create mode 100755 joinbot/deploy create mode 100644 joinbot/joinbot/announce.py create mode 100644 joinbot/joinbot/background.py rename {live-autojoin => joinbot/joinbot}/common.py (100%) create mode 100644 joinbot/joinbot/service.py rename live-autojoin/web.py => joinbot/joinbot/web/__init__.py (99%) rename {live-autojoin => joinbot/joinbot/web}/templates/error.html (100%) rename {live-autojoin => joinbot/joinbot/web}/templates/status.html (100%) create mode 100644 joinbot/pyproject.toml create mode 100644 joinbot/setup.cfg create mode 100644 joinbot/setup.py delete mode 100644 live-autojoin/announce.py delete mode 100644 live-autojoin/background.py delete mode 100755 live-autojoin/deploy delete mode 100644 live-autojoin/service.py delete mode 100644 live-autojoin/systemd/live-autojoin-announce.service delete mode 100644 live-autojoin/systemd/live-autojoin-cron.service delete mode 100644 live-autojoin/systemd/live-autojoin-cron.timer delete mode 100644 live-autojoin/systemd/live-autojoin-service.service delete mode 100644 live-autojoin/systemd/live-autojoin-web.service delete mode 100644 live-autojoin/systemd/live-autojoin-web.socket delete mode 100644 live-autojoin/systemd/live-autojoin.target diff --git a/joinbot/.gitignore b/joinbot/.gitignore new file mode 100644 index 0000000..edd9d60 --- /dev/null +++ b/joinbot/.gitignore @@ -0,0 +1,2 @@ +build/ +dist/ diff --git a/live-autojoin/README.md b/joinbot/README.md similarity index 100% rename from live-autojoin/README.md rename to joinbot/README.md diff --git a/joinbot/deploy b/joinbot/deploy new file mode 100755 index 0000000..02fd157 --- /dev/null +++ b/joinbot/deploy @@ -0,0 +1,7 @@ +#!/bin/bash + +echo "Ok, but why aren't you using a package manager" + +#tar cz systemd | ssh root@bingus.internet6.net. tar xzC /etc/systemd/system +python3 -m build --no-isolation --wheel +tar cC dist joinbot-0.1.0-py3-none-any.whl | ssh anders@bingus.internet6.net. tar xC /var/tmp \&\& sudo pip3 install -U /var/tmp/joinbot-0.1.0-py3-none-any.whl --no-deps \&\& rm /var/tmp/joinbot-0.1.0-py3-none-any.whl diff --git a/joinbot/joinbot/announce.py b/joinbot/joinbot/announce.py new file mode 100644 index 0000000..0953b1c --- /dev/null +++ b/joinbot/joinbot/announce.py @@ -0,0 +1,59 @@ +# announce.py - announce new users + +import select +import urllib.parse +import urllib.request +import urllib.error +import base64 +import json + +from .common import connect_db, service_name + +POLL_INTERVAL=3600 + +def main(): + (cn, cr) = connect_db() + + cr.execute("LISTEN live_autojoin") + + while True: + cn.poll() + cn.notifies.clear() + work = False + while True: + cr.execute("BEGIN") + # - invited less than 1 minute ago + # - no announcement of same user in same event less than 30 days ago + # - user has update permission (not banned) + # - admin has update permission + # - admin authorization has submit scope + cr.execute("""SELECT access_token, admin_username, ticket.event_flake, username FROM live_autojoin_ticket AS ticket JOIN live_autojoin_service ON service_name=name JOIN live_autojoin_admin_authorization USING (service_name) JOIN reddit_app_authorization ON authorization_id=id JOIN reddit_liveupdate_event_contributor AS userrel ON userrel."type"='contributor' AND ticket.event_flake=userrel.event_flake AND username=userrel.name JOIN reddit_liveupdate_event_contributor AS adminrel ON adminrel."type"='contributor' AND ticket.event_flake = adminrel.event_flake AND admin_username = adminrel.name WHERE service_name = %s AND status = 'ok' AND updated_at + '1m' > CURRENT_TIMESTAMP AND NOT EXISTS (SELECT * FROM live_autojoin_announcement WHERE for_username = username AND event_flake = ticket.event_flake AND at + '30d' > updated_at) AND has_permission('update', userrel.permissions) AND has_permission('update', adminrel.permissions) AND array_position(scope, 'submit') IS NOT NULL LIMIT 1""", (service_name,)) + try: + [(access_token, admin_username, event_flake, username)] = cr.fetchall() + except ValueError: + break + else: + work = True + escaped_username = username.replace('_', '\\_') + usertext = f'*[\\/u\\/{ escaped_username }](/user/{ escaped_username }) has joined this thread*' + body = urllib.parse.urlencode({ 'api_type': 'json', 'body': usertext }).encode('utf-8') + req = urllib.request.Request(f'https://oauth.reddit.com/api/live/{event_flake}/update', data=body, method='POST') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + try: + res = json.load(urllib.request.urlopen(req)) + except urllib.error.HTTPError as e: + if e.code == 403: + admin_username = None + usertext = None + else: + raise + cr.execute("INSERT INTO live_autojoin_announcement (at, event_flake, for_username, author, body) VALUES (CURRENT_TIMESTAMP, %s, %s, %s, %s)", (event_flake, username, admin_username, usertext)) + finally: + cn.commit() + + cn.poll() + if work or len(cn.notifies) > 0: + continue + + select.select([cn], [], [], POLL_INTERVAL) diff --git a/joinbot/joinbot/background.py b/joinbot/joinbot/background.py new file mode 100644 index 0000000..abfe534 --- /dev/null +++ b/joinbot/joinbot/background.py @@ -0,0 +1,182 @@ +# background.py - jobs that are not time-critical. +# - refresh access tokens +# - sync PMs +# - sync allowed threads (subreddit links) +# - accept admin invites +# - sync contributor lists +# - TODO sync subreddit bans and moderators +# - TODO sync modmail (to receive joining requests to modmail) + +# roadmap +# - for PM, allowed threads, and modmail sync, create a lightweight version to run frequently to handle new events quickly--distinct from the more expensive "full sync" that is implemented here. + +import urllib.request +import urllib.parse + +import json +import base64 +import re + +from .common import connect_db, service_name + +def main(): + + # + # refresh tokens + # + + (cn, cr) = connect_db() + + def do_refresh_token(client_id, client_secret, refresh_token): + body = urllib.parse.urlencode({ 'grant_type': 'refresh_token', 'refresh_token': refresh_token }).encode('utf-8') + req = urllib.request.Request('https://www.reddit.com/api/v1/access_token', data=body, method='POST') + auth = base64.b64encode('{}:{}'.format(client_id, client_secret).encode('utf-8')).decode('utf-8') + req.add_header('Authorization', 'Basic {}'.format(auth)) + req.add_header('User-Agent', 'autojoin/0.1.0') + res = urllib.request.urlopen(req) + return json.load(res) + + while True: + cr.execute("BEGIN") + cr.execute("SELECT authorization_id, client_id, client_secret, refresh_token FROM live_autojoin_admin_authorization JOIN reddit_app_authorization ON authorization_id=id JOIN reddit_app USING (client_id) WHERE service_name = %s AND refresh_token IS NOT NULL AND expires < CURRENT_TIMESTAMP + '30min' LIMIT 1 FOR NO KEY UPDATE OF reddit_app_authorization", [service_name]) + try: + [(authorization_id, client_id, client_secret, refresh_token)] = cr.fetchall() + except ValueError: + cn.rollback() + break + else: + print('refreshing token for authorization {}'.format(authorization_id)) + new_token = do_refresh_token(client_id, client_secret, refresh_token) + cr.execute("UPDATE reddit_app_authorization SET access_token = %s, refresh_token = %s, scope = regexp_split_to_array(%s, ' ')::reddit_app_scope[], expires = CURRENT_TIMESTAMP + make_interval(secs => %s) WHERE id = %s", (new_token['access_token'], new_token['refresh_token'], new_token['scope'], new_token['expires_in'], authorization_id)) + cn.commit() + + # + # load PMs + # + + def flatten_privatemessage_listing(json_): + assert json_['kind'] == 'Listing' + for p in json_['data']['children']: + assert p['kind'] == 't4' + yield p['data'] + replies = p['data']['replies'] + if replies: + assert replies['kind'] == 'Listing' + for r in replies['data']['children']: + assert p['kind'] == 't4' + yield p['data'] + + def privatemessage_to_tuple(data): + id_ = data['id'] + parent_id = None + if data['parent_id'] is not None: + parent_id = data['parent_id'].split('_',maxsplit=1)[1] + assert int(parent_id, 36) == data['first_message'] + created = data['created_utc'] + sr = data['subreddit'] + author = None if data['author'] == '[deleted]' else data['author'] + if data['dest'].startswith('#'): + # modmail (destination is subreddit) + dest = None + dest_is_sr = True + else: + # destination is user + dest = data['dest'] + dest_is_sr = False + subject = data['subject'] + body = data['body'] + return (id_, parent_id, created, sr, author, dest, dest_is_sr, subject, body) + + cr.execute("BEGIN") + cr.execute("SELECT sr, access_token FROM live_autojoin_service JOIN live_autojoin_admin_authorization ON name = service_name JOIN reddit_app_authorization ON authorization_id=id JOIN reddit_app ON reddit_app.client_id=reddit_app_authorization.client_id WHERE service_name = %s", [service_name]) + [(sr, access_token)] = cr.fetchall() + cr.execute('CREATE TEMPORARY TABLE privatemessage_load (LIKE reddit_privatemessage INCLUDING INDEXES)') + after = None + while True: + if after: + rsc = 'https://oauth.reddit.com/message/messages?raw_json=1&limit=100&after={}'.format(after) + else: + rsc = 'https://oauth.reddit.com/message/messages?raw_json=1&limit=100' + req = urllib.request.Request(rsc, method='GET') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + res = json.load(urllib.request.urlopen(req)) + tuples = (privatemessage_to_tuple(d) for d in flatten_privatemessage_listing(res)) + cr.executemany('INSERT INTO privatemessage_load (id, parent_id, created, sr, author, dest, dest_is_sr, subject, body) VALUES (%s, %s, to_timestamp(%s), %s, %s, %s, %s, %s, %s)', tuples) + if 'after' in res: + after = res['after'] + else: + break + cr.execute("INSERT INTO reddit_privatemessage (id, parent_id, created, sr, author, dest, dest_is_sr, subject, body) SELECT id, parent_id, created, sr, author, dest, dest_is_sr, subject, body FROM privatemessage_load ON CONFLICT ON CONSTRAINT reddit_privatemessage_pkey DO NOTHING") + cr.execute("DROP TABLE privatemessage_load") + cn.commit() + + # + # build allowed thread list + # TODO look beyond first page + # TODO wiki config page for additional threads + # + + def flake_from_url(url_): + result = re.fullmatch('https?://[a-z]+\.reddit\.com/live/([a-z0-9]+)/?(?:\?.*)?', url_) + return result and result.group(1) + + def allowed_threads(): + req = urllib.request.Request('https://oauth.reddit.com/r/livecounting/search?q=url%3Alive+site%3Areddit.com+self%3Ano&restrict_sr=on&include_over_18=on&sort=new&t=all&limit=100', method='GET') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + res = json.load(urllib.request.urlopen(req)) + flakes = (flake_from_url(thing['data']['url']) for thing in res['data']['children'] if thing['data']['is_self'] is False) + return set((f for f in flakes if f)) + + cr.execute("BEGIN") + #cr.execute("DELETE FROM live_autojoin_allowed_event WHERE service_name = %s", (service_name,)) + cr.executemany("INSERT INTO live_autojoin_allowed_event (service_name, event_flake) VALUES (%s, %s) ON CONFLICT (service_name, event_flake) DO NOTHING", ((service_name, f) for f in allowed_threads())) + cn.commit() + + # accept admin invites + + cr.execute("BEGIN") + cr.execute("SELECT id, body FROM reddit_privatemessage JOIN live_autojoin_service ON dest = admin_username WHERE author = 'reddit' AND NOT dest_is_sr AND parent_id IS NULL AND subject LIKE 'invitation to contribute to %%' AND NOT EXISTS (SELECT * FROM live_autojoin_admin_invite WHERE privatemessage_id = id AND name = %s)", (service_name,)) + for (id_, body) in cr.fetchall(): + match = re.search('/live/([a-z0-9]+)/contributors', body) + event_flake = match and match.group(1) + result = None + if event_flake: + body = urllib.parse.urlencode({ 'api_type': 'json' }).encode('utf-8') + req = urllib.request.Request('https://oauth.reddit.com/api/live/{}/accept_contributor_invite'.format(event_flake), method='POST', data=body) + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + urllib.request.urlopen(req) + result = 'ok' + cr.execute("INSERT INTO live_autojoin_admin_invite (privatemessage_id, event_flake, result) VALUES (%s, %s, %s)", [id_, event_flake, result]) + cn.commit() + + # load contributor lists + + cr.execute("BEGIN") + cr.execute("SELECT event_flake FROM live_autojoin_allowed_event WHERE service_name = %s", (service_name,)) + cn.commit() + for (event_flake,) in cr.fetchall(): + req = urllib.request.Request('https://oauth.reddit.com/live/{}/contributors'.format(event_flake), method='GET') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + contributors_lists = json.load(urllib.request.urlopen(req)) + # endpoint returns two listings, contributors and contributor _invites_, in JSON sequence. + # if not a contributor, cannot see contributor invites. + # in that case, no JSON sequence--simply the single listing is returned--annoying. + if isinstance(contributors_lists, dict): + empty_listing = {'kind': 'UserList', 'data': {'children': []}} + contributors_lists = [contributors_lists, empty_listing] + assert all((l['kind'] == 'UserList' for l in contributors_lists)) + contributors = ((event_flake, c['name'], '{{{}}}'.format(','.join(c['permissions'])), ty) for (ty, l) in zip(('contributor', 'invite'), contributors_lists) for c in l['data']['children']) + cr.execute("BEGIN") + cr.execute("DELETE FROM reddit_liveupdate_event_contributor WHERE event_flake = %s", (event_flake,)) + cr.executemany("INSERT INTO reddit_liveupdate_event_contributor (event_flake, name, permissions, \"type\") VALUES (%s, %s, %s::text[], %s)", contributors) + cn.commit() + print(event_flake) + + # TODO load subreddit bans (and moderators) + # TODO load modmail for joining requests + + cn.close() diff --git a/live-autojoin/common.py b/joinbot/joinbot/common.py similarity index 100% rename from live-autojoin/common.py rename to joinbot/joinbot/common.py diff --git a/joinbot/joinbot/service.py b/joinbot/joinbot/service.py new file mode 100644 index 0000000..8246ead --- /dev/null +++ b/joinbot/joinbot/service.py @@ -0,0 +1,161 @@ +# service.py - invite processing + +#- exchange authentication codes +#- fetch authorization identity to populate ticket username +#- invite users +#- accept invites + +import select +import urllib.parse +import urllib.request +import urllib.error +import base64 +import json + +from .common import connect_db, service_name + +POLL_INTERVAL=3600 + +def main(): + (cn, cr) = connect_db() + + cr.execute("LISTEN live_autojoin") + + while True: + cn.poll() + cn.notifies.clear() + work = False + while True: + cr.execute("BEGIN") + cr.execute("SELECT client_id, client_secret, redirect_uri, id, code FROM live_autojoin_service JOIN reddit_app USING (client_id) JOIN live_autojoin_ticket ON name = service_name JOIN reddit_app_authorization_code ON id = authorization_code_id WHERE authorization_id IS NULL AND service_name = %s LIMIT 1 FOR UPDATE OF live_autojoin_ticket, reddit_app_authorization_code", (service_name,)) + try: + [(client_id, client_secret, redirect_uri, authorization_code_id, code)] = cr.fetchall() + except ValueError: + break + else: + work = True + body = urllib.parse.urlencode({ 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': redirect_uri }).encode('utf-8') + req = urllib.request.Request('https://www.reddit.com/api/v1/access_token', data=body, method='POST') + auth = base64.b64encode('{}:{}'.format(client_id, client_secret).encode('utf-8')).decode('utf-8') + req.add_header('Authorization', 'Basic {}'.format(auth)) + req.add_header('User-Agent', 'autojoin/0.1.0') + try: + res = json.load(urllib.request.urlopen(req)) + except urllib.error.HTTPError as e: + if e.code == 404: + res = {'error': 'invalid_grant'} # supposing 404 really means this + else: + raise + if 'error' in res: + if res['error'] == 'invalid_grant': + cr.execute("DELETE FROM reddit_app_authorization_code WHERE id = %s", (authorization_code_id,)) + else: + raise ValueError("unrecognized error '{}'".format(res['error'])) + else: + assert res['token_type'] == 'bearer' + cr.execute("WITH q1 AS (INSERT INTO reddit_app_authorization (client_id, access_token, scope, expires) VALUES (%s, %s, regexp_split_to_array(%s, ' ')::reddit_app_scope[], CURRENT_TIMESTAMP + make_interval(secs => %s)) RETURNING id) UPDATE reddit_app_authorization_code SET authorization_id = q1.id FROM q1 WHERE reddit_app_authorization_code.id = %s", (client_id, res['access_token'], res['scope'], res['expires_in'], authorization_code_id)) + finally: + cn.commit() + + while True: + cr.execute("BEGIN") + cr.execute("SELECT reddit_app_authorization.id, access_token, ticket FROM live_autojoin_ticket JOIN reddit_app_authorization_code ON reddit_app_authorization_code.id=authorization_code_id JOIN reddit_app_authorization ON reddit_app_authorization.id = authorization_id WHERE service_name = %s AND username IS NULL AND array_position(scope, 'identity') IS NOT NULL LIMIT 1 FOR UPDATE OF live_autojoin_ticket", (service_name,)) + try: + [(authorization_id, access_token, ticket)] = cr.fetchall() + except ValueError: + break + else: + work = True + req = urllib.request.Request('https://oauth.reddit.com/api/v1/me', method='GET') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + try: + res = json.load(urllib.request.urlopen(req)) + except urllib.error.HTTPError as e: + if e.code == 401: + cr.execute("DELETE FROM reddit_app_authorization WHERE id = %s", (authorization_id,)) + else: + raise + else: + cr.execute("UPDATE live_autojoin_ticket SET username = %s WHERE service_name = %s AND ticket = %s", (res['name'], service_name, ticket)) + finally: + cn.commit() + + while True: + cr.execute("BEGIN") + cr.execute("SELECT access_token, ticket, event_flake, username FROM live_autojoin_ticket LEFT OUTER JOIN LATERAL (SELECT service_name, access_token FROM live_autojoin_admin_authorization JOIN reddit_app_authorization ON id=authorization_id WHERE service_name = live_autojoin_ticket.service_name LIMIT 1) q1 USING (service_name) WHERE service_name = %s AND username IS NOT NULL AND status IS NULL LIMIT 1 FOR NO KEY UPDATE OF live_autojoin_ticket", (service_name,)) + try: + [(access_token, ticket, event, username)] = cr.fetchall() + except ValueError: + break + else: + work = True + if access_token is None: + result = 'noadmin' + else: + body = urllib.parse.urlencode({ 'api_type': 'json', 'name': username, 'permissions': '+update', 'type': 'liveupdate_contributor_invite' }).encode('utf-8') + req = urllib.request.Request('https://oauth.reddit.com/api/live/{}/invite_contributor'.format(event), data=body, method='POST') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + try: + res = json.load(urllib.request.urlopen(req)) + except urllib.error.HTTPError as e: + if e.code == 403: + result = 'noadmin' + else: + raise + else: + errors = [er[0] for er in res['json']['errors']] + if not errors: + result = 'invited' + elif errors == ['LIVEUPDATE_ALREADY_CONTRIBUTOR']: + result = 'already_contributor_or_invited' + else: + raise RuntimeError(str(errors)) + if result == 'invited': + cr.execute("DELETE FROM reddit_liveupdate_event_contributor WHERE event_flake = %s AND name = %s", (event, username)) + cr.execute("INSERT INTO reddit_liveupdate_event_contributor (event_flake, name, permissions, type) VALUES (%s, %s, liveupdate_permission_set '+update', 'invite') ON CONFLICT DO NOTHING", (event, username)) + cr.execute("UPDATE live_autojoin_ticket SET status = %s, updated_at = CURRENT_TIMESTAMP WHERE service_name = %s AND ticket = %s", (result, service_name, ticket)) + finally: + cn.commit() + + while True: + cr.execute("BEGIN") + cr.execute("SELECT reddit_app_authorization.id, access_token, ticket, event_flake, username, status FROM live_autojoin_ticket JOIN reddit_app_authorization_code ON reddit_app_authorization_code.id=authorization_code_id JOIN reddit_app_authorization ON reddit_app_authorization.id = authorization_id WHERE service_name = %s AND status IN ('invited', 'already_contributor_or_invited') AND array_position(scope, 'livemanage') IS NOT NULL LIMIT 1 FOR NO KEY UPDATE OF live_autojoin_ticket", (service_name,)) + try: + [(authorization_id, access_token, ticket, event_flake, username, status)] = cr.fetchall() + except ValueError: + break + else: + work = True + body = urllib.parse.urlencode({ 'api_type': 'json' }).encode('utf-8') + req = urllib.request.Request('https://oauth.reddit.com/api/live/{}/accept_contributor_invite'.format(event_flake), data=body, method='POST') + req.add_header('Authorization', 'Bearer {}'.format(access_token)) + req.add_header('User-Agent', 'autojoin/0.1.0') + try: + res = json.load(urllib.request.urlopen(req)) + except urllib.error.HTTPError as e: + if e.code == 401: + cr.execute("DELETE FROM reddit_app_authorization WHERE id = %s", (authorization_id,)) + else: + raise + else: + errors = [er[0] for er in res['json']['errors']] + if not errors: + result = 'ok' + elif errors == ['LIVEUPDATE_NO_INVITE_FOUND']: + result = 'already_contributor' if status == 'already_contributor_or_invited' else None + else: + raise RuntimeError(str(errors)) + if result == 'ok': + cr.execute("UPDATE reddit_liveupdate_event_contributor SET type = 'contributor' WHERE event_flake = %s AND name = %s", (event, username)) + cr.execute("NOTIFY live_autojoin") # for announcements + cr.execute("UPDATE live_autojoin_ticket SET status = %s, updated_at = CURRENT_TIMESTAMP WHERE service_name = %s AND ticket = %s", (result, service_name, ticket)) + finally: + cn.commit() + + cn.poll() + if work or len(cn.notifies) > 0: + continue + + select.select([cn], [], [], POLL_INTERVAL) diff --git a/live-autojoin/web.py b/joinbot/joinbot/web/__init__.py similarity index 99% rename from live-autojoin/web.py rename to joinbot/joinbot/web/__init__.py index 819cf97..6d0f74f 100644 --- a/live-autojoin/web.py +++ b/joinbot/joinbot/web/__init__.py @@ -4,7 +4,7 @@ import secrets import re import urllib.parse -from common import connect_db +from ..common import connect_db DEFAULT_SERVICE = 'autojoin' diff --git a/live-autojoin/templates/error.html b/joinbot/joinbot/web/templates/error.html similarity index 100% rename from live-autojoin/templates/error.html rename to joinbot/joinbot/web/templates/error.html diff --git a/live-autojoin/templates/status.html b/joinbot/joinbot/web/templates/status.html similarity index 100% rename from live-autojoin/templates/status.html rename to joinbot/joinbot/web/templates/status.html diff --git a/joinbot/pyproject.toml b/joinbot/pyproject.toml new file mode 100644 index 0000000..8fe2f47 --- /dev/null +++ b/joinbot/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/joinbot/setup.cfg b/joinbot/setup.cfg new file mode 100644 index 0000000..f81d928 --- /dev/null +++ b/joinbot/setup.cfg @@ -0,0 +1,22 @@ +[metadata] +name = joinbot +version = 0.1.0 + +[options] +packages = + joinbot + joinbot.web +install_requires = + psycopg2 ~= 2.8 + flask ~= 2.2 +include_package_data = True + +[options.package_data] +joinbot.web = + templates/*.html + +[options.entry_points] +console_scripts = + joinbot-service = joinbot.service:main + joinbot-background = joinbot.background:main + joinbot-announce = joinbot.announce:main diff --git a/joinbot/setup.py b/joinbot/setup.py new file mode 100644 index 0000000..b908cbe --- /dev/null +++ b/joinbot/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup() diff --git a/live-autojoin/announce.py b/live-autojoin/announce.py deleted file mode 100644 index 047ba77..0000000 --- a/live-autojoin/announce.py +++ /dev/null @@ -1,58 +0,0 @@ -# announce.py - announce new users - -import select -import urllib.parse -import urllib.request -import urllib.error -import base64 -import json - -from common import connect_db, service_name - -POLL_INTERVAL=3600 - -(cn, cr) = connect_db() - -cr.execute("LISTEN live_autojoin") - -while True: - cn.poll() - cn.notifies.clear() - work = False - while True: - cr.execute("BEGIN") - # - invited less than 1 minute ago - # - no announcement of same user in same event less than 30 days ago - # - user has update permission (not banned) - # - admin has update permission - # - admin authorization has submit scope - cr.execute("""SELECT access_token, admin_username, ticket.event_flake, username FROM live_autojoin_ticket AS ticket JOIN live_autojoin_service ON service_name=name JOIN live_autojoin_admin_authorization USING (service_name) JOIN reddit_app_authorization ON authorization_id=id JOIN reddit_liveupdate_event_contributor AS userrel ON userrel."type"='contributor' AND ticket.event_flake=userrel.event_flake AND username=userrel.name JOIN reddit_liveupdate_event_contributor AS adminrel ON adminrel."type"='contributor' AND ticket.event_flake = adminrel.event_flake AND admin_username = adminrel.name WHERE service_name = %s AND status = 'ok' AND updated_at + '1m' > CURRENT_TIMESTAMP AND NOT EXISTS (SELECT * FROM live_autojoin_announcement WHERE for_username = username AND event_flake = ticket.event_flake AND at + '30d' > updated_at) AND has_permission('update', userrel.permissions) AND has_permission('update', adminrel.permissions) AND array_position(scope, 'submit') IS NOT NULL LIMIT 1""", (service_name,)) - try: - [(access_token, admin_username, event_flake, username)] = cr.fetchall() - except ValueError: - break - else: - work = True - escaped_username = username.replace('_', '\\_') - usertext = f'*[\\/u\\/{ escaped_username }](/user/{ escaped_username }) has joined this thread*' - body = urllib.parse.urlencode({ 'api_type': 'json', 'body': usertext }).encode('utf-8') - req = urllib.request.Request(f'https://oauth.reddit.com/api/live/{event_flake}/update', data=body, method='POST') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - try: - res = json.load(urllib.request.urlopen(req)) - except urllib.error.HTTPError as e: - if e.code == 403: - admin_username = None - usertext = None - else: - raise - cr.execute("INSERT INTO live_autojoin_announcement (at, event_flake, for_username, author, body) VALUES (CURRENT_TIMESTAMP, %s, %s, %s, %s)", (event_flake, username, admin_username, usertext)) - finally: - cn.commit() - - cn.poll() - if work or len(cn.notifies) > 0: - continue - - select.select([cn], [], [], POLL_INTERVAL) diff --git a/live-autojoin/background.py b/live-autojoin/background.py deleted file mode 100644 index f5a9a87..0000000 --- a/live-autojoin/background.py +++ /dev/null @@ -1,180 +0,0 @@ -# background.py - jobs that are not time-critical. -# - refresh access tokens -# - sync PMs -# - sync allowed threads (subreddit links) -# - accept admin invites -# - sync contributor lists -# - TODO sync subreddit bans and moderators -# - TODO sync modmail (to receive joining requests to modmail) - -# roadmap -# - for PM, allowed threads, and modmail sync, create a lightweight version to run frequently to handle new events quickly--distinct from the more expensive "full sync" that is implemented here. - -import urllib.request -import urllib.parse - -import json -import base64 -import re - -from common import connect_db, service_name - -# -# refresh tokens -# - -(cn, cr) = connect_db() - -def do_refresh_token(client_id, client_secret, refresh_token): - body = urllib.parse.urlencode({ 'grant_type': 'refresh_token', 'refresh_token': refresh_token }).encode('utf-8') - req = urllib.request.Request('https://www.reddit.com/api/v1/access_token', data=body, method='POST') - auth = base64.b64encode('{}:{}'.format(client_id, client_secret).encode('utf-8')).decode('utf-8') - req.add_header('Authorization', 'Basic {}'.format(auth)) - req.add_header('User-Agent', 'autojoin/0.1.0') - res = urllib.request.urlopen(req) - return json.load(res) - -while True: - cr.execute("BEGIN") - cr.execute("SELECT authorization_id, client_id, client_secret, refresh_token FROM live_autojoin_admin_authorization JOIN reddit_app_authorization ON authorization_id=id JOIN reddit_app USING (client_id) WHERE service_name = %s AND refresh_token IS NOT NULL AND expires < CURRENT_TIMESTAMP + '30min' LIMIT 1 FOR NO KEY UPDATE OF reddit_app_authorization", [service_name]) - try: - [(authorization_id, client_id, client_secret, refresh_token)] = cr.fetchall() - except ValueError: - cn.rollback() - break - else: - print('refreshing token for authorization {}'.format(authorization_id)) - new_token = do_refresh_token(client_id, client_secret, refresh_token) - cr.execute("UPDATE reddit_app_authorization SET access_token = %s, refresh_token = %s, scope = regexp_split_to_array(%s, ' ')::reddit_app_scope[], expires = CURRENT_TIMESTAMP + make_interval(secs => %s) WHERE id = %s", (new_token['access_token'], new_token['refresh_token'], new_token['scope'], new_token['expires_in'], authorization_id)) - cn.commit() - -# -# load PMs -# - -def flatten_privatemessage_listing(json_): - assert json_['kind'] == 'Listing' - for p in json_['data']['children']: - assert p['kind'] == 't4' - yield p['data'] - replies = p['data']['replies'] - if replies: - assert replies['kind'] == 'Listing' - for r in replies['data']['children']: - assert p['kind'] == 't4' - yield p['data'] - -def privatemessage_to_tuple(data): - id_ = data['id'] - parent_id = None - if data['parent_id'] is not None: - parent_id = data['parent_id'].split('_',maxsplit=1)[1] - assert int(parent_id, 36) == data['first_message'] - created = data['created_utc'] - sr = data['subreddit'] - author = None if data['author'] == '[deleted]' else data['author'] - if data['dest'].startswith('#'): - # modmail (destination is subreddit) - dest = None - dest_is_sr = True - else: - # destination is user - dest = data['dest'] - dest_is_sr = False - subject = data['subject'] - body = data['body'] - return (id_, parent_id, created, sr, author, dest, dest_is_sr, subject, body) - -cr.execute("BEGIN") -cr.execute("SELECT sr, access_token FROM live_autojoin_service JOIN live_autojoin_admin_authorization ON name = service_name JOIN reddit_app_authorization ON authorization_id=id JOIN reddit_app ON reddit_app.client_id=reddit_app_authorization.client_id WHERE service_name = %s", [service_name]) -[(sr, access_token)] = cr.fetchall() -cr.execute('CREATE TEMPORARY TABLE privatemessage_load (LIKE reddit_privatemessage INCLUDING INDEXES)') -after = None -while True: - if after: - rsc = 'https://oauth.reddit.com/message/messages?raw_json=1&limit=100&after={}'.format(after) - else: - rsc = 'https://oauth.reddit.com/message/messages?raw_json=1&limit=100' - req = urllib.request.Request(rsc, method='GET') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - res = json.load(urllib.request.urlopen(req)) - tuples = (privatemessage_to_tuple(d) for d in flatten_privatemessage_listing(res)) - cr.executemany('INSERT INTO privatemessage_load (id, parent_id, created, sr, author, dest, dest_is_sr, subject, body) VALUES (%s, %s, to_timestamp(%s), %s, %s, %s, %s, %s, %s)', tuples) - if 'after' in res: - after = res['after'] - else: - break -cr.execute("INSERT INTO reddit_privatemessage (id, parent_id, created, sr, author, dest, dest_is_sr, subject, body) SELECT id, parent_id, created, sr, author, dest, dest_is_sr, subject, body FROM privatemessage_load ON CONFLICT ON CONSTRAINT reddit_privatemessage_pkey DO NOTHING") -cr.execute("DROP TABLE privatemessage_load") -cn.commit() - -# -# build allowed thread list -# TODO look beyond first page -# TODO wiki config page for additional threads -# - -def flake_from_url(url_): - result = re.fullmatch('https?://[a-z]+\.reddit\.com/live/([a-z0-9]+)/?(?:\?.*)?', url_) - return result and result.group(1) - -def allowed_threads(): - req = urllib.request.Request('https://oauth.reddit.com/r/livecounting/search?q=url%3Alive+site%3Areddit.com+self%3Ano&restrict_sr=on&include_over_18=on&sort=new&t=all&limit=100', method='GET') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - res = json.load(urllib.request.urlopen(req)) - flakes = (flake_from_url(thing['data']['url']) for thing in res['data']['children'] if thing['data']['is_self'] is False) - return set((f for f in flakes if f)) - -cr.execute("BEGIN") -#cr.execute("DELETE FROM live_autojoin_allowed_event WHERE service_name = %s", (service_name,)) -cr.executemany("INSERT INTO live_autojoin_allowed_event (service_name, event_flake) VALUES (%s, %s) ON CONFLICT (service_name, event_flake) DO NOTHING", ((service_name, f) for f in allowed_threads())) -cn.commit() - -# accept admin invites - -cr.execute("BEGIN") -cr.execute("SELECT id, body FROM reddit_privatemessage JOIN live_autojoin_service ON dest = admin_username WHERE author = 'reddit' AND NOT dest_is_sr AND parent_id IS NULL AND subject LIKE 'invitation to contribute to %%' AND NOT EXISTS (SELECT * FROM live_autojoin_admin_invite WHERE privatemessage_id = id AND name = %s)", (service_name,)) -for (id_, body) in cr.fetchall(): - match = re.search('/live/([a-z0-9]+)/contributors', body) - event_flake = match and match.group(1) - result = None - if event_flake: - body = urllib.parse.urlencode({ 'api_type': 'json' }).encode('utf-8') - req = urllib.request.Request('https://oauth.reddit.com/api/live/{}/accept_contributor_invite'.format(event_flake), method='POST', data=body) - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - urllib.request.urlopen(req) - result = 'ok' - cr.execute("INSERT INTO live_autojoin_admin_invite (privatemessage_id, event_flake, result) VALUES (%s, %s, %s)", [id_, event_flake, result]) -cn.commit() - -# load contributor lists - -cr.execute("BEGIN") -cr.execute("SELECT event_flake FROM live_autojoin_allowed_event WHERE service_name = %s", (service_name,)) -cn.commit() -for (event_flake,) in cr.fetchall(): - req = urllib.request.Request('https://oauth.reddit.com/live/{}/contributors'.format(event_flake), method='GET') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - contributors_lists = json.load(urllib.request.urlopen(req)) - # endpoint returns two listings, contributors and contributor _invites_, in JSON sequence. - # if not a contributor, cannot see contributor invites. - # in that case, no JSON sequence--simply the single listing is returned--annoying. - if isinstance(contributors_lists, dict): - empty_listing = {'kind': 'UserList', 'data': {'children': []}} - contributors_lists = [contributors_lists, empty_listing] - assert all((l['kind'] == 'UserList' for l in contributors_lists)) - contributors = ((event_flake, c['name'], '{{{}}}'.format(','.join(c['permissions'])), ty) for (ty, l) in zip(('contributor', 'invite'), contributors_lists) for c in l['data']['children']) - cr.execute("BEGIN") - cr.execute("DELETE FROM reddit_liveupdate_event_contributor WHERE event_flake = %s", (event_flake,)) - cr.executemany("INSERT INTO reddit_liveupdate_event_contributor (event_flake, name, permissions, \"type\") VALUES (%s, %s, %s::text[], %s)", contributors) - cn.commit() - print(event_flake) - -# TODO load subreddit bans (and moderators) -# TODO load modmail for joining requests - -cn.close() diff --git a/live-autojoin/deploy b/live-autojoin/deploy deleted file mode 100755 index eceeb7a..0000000 --- a/live-autojoin/deploy +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -echo "Ok, but why aren't you using a package manager" - -tar cz {cert,privkey}.pem {background,service,announce,common,web}.py templates systemd | ssh root@hatnd.acorn.pw tar xzC /opt/live-autojoin diff --git a/live-autojoin/service.py b/live-autojoin/service.py deleted file mode 100644 index a56c7b1..0000000 --- a/live-autojoin/service.py +++ /dev/null @@ -1,154 +0,0 @@ -# service.py - invite processing - -#- exchange authentication codes -#- fetch authorization identity to populate ticket username -#- invite users -#- accept invites - -import select -import urllib.parse -import urllib.request -import urllib.error -import base64 -import json - -from common import connect_db, service_name - -POLL_INTERVAL=3600 - -(cn, cr) = connect_db() - -cr.execute("LISTEN live_autojoin") - -while True: - cn.poll() - cn.notifies.clear() - work = False - while True: - cr.execute("BEGIN") - cr.execute("SELECT client_id, client_secret, redirect_uri, id, code FROM live_autojoin_service JOIN reddit_app USING (client_id) JOIN live_autojoin_ticket ON name = service_name JOIN reddit_app_authorization_code ON id = authorization_code_id WHERE authorization_id IS NULL AND service_name = %s LIMIT 1 FOR UPDATE OF live_autojoin_ticket, reddit_app_authorization_code", (service_name,)) - try: - [(client_id, client_secret, redirect_uri, authorization_code_id, code)] = cr.fetchall() - except ValueError: - break - else: - work = True - body = urllib.parse.urlencode({ 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': redirect_uri }).encode('utf-8') - req = urllib.request.Request('https://www.reddit.com/api/v1/access_token', data=body, method='POST') - auth = base64.b64encode('{}:{}'.format(client_id, client_secret).encode('utf-8')).decode('utf-8') - req.add_header('Authorization', 'Basic {}'.format(auth)) - req.add_header('User-Agent', 'autojoin/0.1.0') - res = json.load(urllib.request.urlopen(req)) - if 'error' in res: - if res['error'] == 'invalid_grant': - cr.execute("DELETE FROM reddit_app_authorization_code WHERE id = %s", (authorization_code_id,)) - else: - raise ValueError("unrecognized error '{}'".format(res['error'])) - else: - assert res['token_type'] == 'bearer' - cr.execute("WITH q1 AS (INSERT INTO reddit_app_authorization (client_id, access_token, scope, expires) VALUES (%s, %s, regexp_split_to_array(%s, ' ')::reddit_app_scope[], CURRENT_TIMESTAMP + make_interval(secs => %s)) RETURNING id) UPDATE reddit_app_authorization_code SET authorization_id = q1.id FROM q1 WHERE reddit_app_authorization_code.id = %s", (client_id, res['access_token'], res['scope'], res['expires_in'], authorization_code_id)) - finally: - cn.commit() - - while True: - cr.execute("BEGIN") - cr.execute("SELECT reddit_app_authorization.id, access_token, ticket FROM live_autojoin_ticket JOIN reddit_app_authorization_code ON reddit_app_authorization_code.id=authorization_code_id JOIN reddit_app_authorization ON reddit_app_authorization.id = authorization_id WHERE service_name = %s AND username IS NULL AND array_position(scope, 'identity') IS NOT NULL LIMIT 1 FOR UPDATE OF live_autojoin_ticket", (service_name,)) - try: - [(authorization_id, access_token, ticket)] = cr.fetchall() - except ValueError: - break - else: - work = True - req = urllib.request.Request('https://oauth.reddit.com/api/v1/me', method='GET') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - try: - res = json.load(urllib.request.urlopen(req)) - except urllib.error.HTTPError as e: - if e.code == 401: - cr.execute("DELETE FROM reddit_app_authorization WHERE id = %s", (authorization_id,)) - else: - raise - else: - cr.execute("UPDATE live_autojoin_ticket SET username = %s WHERE service_name = %s AND ticket = %s", (res['name'], service_name, ticket)) - finally: - cn.commit() - - while True: - cr.execute("BEGIN") - cr.execute("SELECT access_token, ticket, event_flake, username FROM live_autojoin_ticket LEFT OUTER JOIN LATERAL (SELECT service_name, access_token FROM live_autojoin_admin_authorization JOIN reddit_app_authorization ON id=authorization_id WHERE service_name = live_autojoin_ticket.service_name LIMIT 1) q1 USING (service_name) WHERE service_name = %s AND username IS NOT NULL AND status IS NULL LIMIT 1 FOR NO KEY UPDATE OF live_autojoin_ticket", (service_name,)) - try: - [(access_token, ticket, event, username)] = cr.fetchall() - except ValueError: - break - else: - work = True - if access_token is None: - result = 'noadmin' - else: - body = urllib.parse.urlencode({ 'api_type': 'json', 'name': username, 'permissions': '+update', 'type': 'liveupdate_contributor_invite' }).encode('utf-8') - req = urllib.request.Request('https://oauth.reddit.com/api/live/{}/invite_contributor'.format(event), data=body, method='POST') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - try: - res = json.load(urllib.request.urlopen(req)) - except urllib.error.HTTPError as e: - if e.code == 403: - result = 'noadmin' - else: - raise - else: - errors = [er[0] for er in res['json']['errors']] - if not errors: - result = 'invited' - elif errors == ['LIVEUPDATE_ALREADY_CONTRIBUTOR']: - result = 'already_contributor_or_invited' - else: - raise RuntimeError(str(errors)) - if result == 'invited': - cr.execute("DELETE FROM reddit_liveupdate_event_contributor WHERE event_flake = %s AND name = %s", (event, username)) - cr.execute("INSERT INTO reddit_liveupdate_event_contributor (event_flake, name, permissions, type) VALUES (%s, %s, liveupdate_permission_set '+update', 'invite') ON CONFLICT DO NOTHING", (event, username)) - cr.execute("UPDATE live_autojoin_ticket SET status = %s, updated_at = CURRENT_TIMESTAMP WHERE service_name = %s AND ticket = %s", (result, service_name, ticket)) - finally: - cn.commit() - - while True: - cr.execute("BEGIN") - cr.execute("SELECT reddit_app_authorization.id, access_token, ticket, event_flake, username, status FROM live_autojoin_ticket JOIN reddit_app_authorization_code ON reddit_app_authorization_code.id=authorization_code_id JOIN reddit_app_authorization ON reddit_app_authorization.id = authorization_id WHERE service_name = %s AND status IN ('invited', 'already_contributor_or_invited') AND array_position(scope, 'livemanage') IS NOT NULL LIMIT 1 FOR NO KEY UPDATE OF live_autojoin_ticket", (service_name,)) - try: - [(authorization_id, access_token, ticket, event_flake, username, status)] = cr.fetchall() - except ValueError: - break - else: - work = True - body = urllib.parse.urlencode({ 'api_type': 'json' }).encode('utf-8') - req = urllib.request.Request('https://oauth.reddit.com/api/live/{}/accept_contributor_invite'.format(event_flake), data=body, method='POST') - req.add_header('Authorization', 'Bearer {}'.format(access_token)) - req.add_header('User-Agent', 'autojoin/0.1.0') - try: - res = json.load(urllib.request.urlopen(req)) - except urllib.error.HTTPError as e: - if e.code == 401: - cr.execute("DELETE FROM reddit_app_authorization WHERE id = %s", (authorization_id,)) - else: - raise - else: - errors = [er[0] for er in res['json']['errors']] - if not errors: - result = 'ok' - elif errors == ['LIVEUPDATE_NO_INVITE_FOUND']: - result = 'already_contributor' if status == 'already_contributor_or_invited' else None - else: - raise RuntimeError(str(errors)) - if result == 'ok': - cr.execute("UPDATE reddit_liveupdate_event_contributor SET type = 'contributor' WHERE event_flake = %s AND name = %s", (event, username)) - cr.execute("NOTIFY live_autojoin") # for announcements - cr.execute("UPDATE live_autojoin_ticket SET status = %s, updated_at = CURRENT_TIMESTAMP WHERE service_name = %s AND ticket = %s", (result, service_name, ticket)) - finally: - cn.commit() - - cn.poll() - if work or len(cn.notifies) > 0: - continue - - select.select([cn], [], [], POLL_INTERVAL) diff --git a/live-autojoin/systemd/live-autojoin-announce.service b/live-autojoin/systemd/live-autojoin-announce.service deleted file mode 100644 index e00569c..0000000 --- a/live-autojoin/systemd/live-autojoin-announce.service +++ /dev/null @@ -1,8 +0,0 @@ -[Service] -WorkingDirectory=/opt/live-autojoin -User=counting -Group=counting -Environment=LIVEAUTOJOINSERVICE=autojoin -Type=simple -ExecStart=python3 announce.py -Restart=always diff --git a/live-autojoin/systemd/live-autojoin-cron.service b/live-autojoin/systemd/live-autojoin-cron.service deleted file mode 100644 index 929dbe3..0000000 --- a/live-autojoin/systemd/live-autojoin-cron.service +++ /dev/null @@ -1,7 +0,0 @@ -[Service] -WorkingDirectory=/opt/live-autojoin -User=counting -Group=counting -Environment=LIVEAUTOJOINSERVICE=autojoin -Type=oneshot -ExecStart=python3 background.py diff --git a/live-autojoin/systemd/live-autojoin-cron.timer b/live-autojoin/systemd/live-autojoin-cron.timer deleted file mode 100644 index 5843509..0000000 --- a/live-autojoin/systemd/live-autojoin-cron.timer +++ /dev/null @@ -1,3 +0,0 @@ -[Timer] -OnActiveSec=0 -OnUnitActiveSec=25min diff --git a/live-autojoin/systemd/live-autojoin-service.service b/live-autojoin/systemd/live-autojoin-service.service deleted file mode 100644 index d69c607..0000000 --- a/live-autojoin/systemd/live-autojoin-service.service +++ /dev/null @@ -1,8 +0,0 @@ -[Service] -WorkingDirectory=/opt/live-autojoin -User=counting -Group=counting -Environment=LIVEAUTOJOINSERVICE=autojoin -Type=simple -ExecStart=python3 service.py -Restart=always diff --git a/live-autojoin/systemd/live-autojoin-web.service b/live-autojoin/systemd/live-autojoin-web.service deleted file mode 100644 index 7b1ebba..0000000 --- a/live-autojoin/systemd/live-autojoin-web.service +++ /dev/null @@ -1,7 +0,0 @@ -[Service] -WorkingDirectory=/opt/live-autojoin -User=counting -Group=counting -Environment=SCRIPT_NAME=/autojoin LIVEAUTOJOINSERVICE=autojoin -Type=simple # `notify` if gunicorn >= 20 -ExecStart=/usr/bin/gunicorn3 web:app diff --git a/live-autojoin/systemd/live-autojoin-web.socket b/live-autojoin/systemd/live-autojoin-web.socket deleted file mode 100644 index be9f1a0..0000000 --- a/live-autojoin/systemd/live-autojoin-web.socket +++ /dev/null @@ -1,4 +0,0 @@ -[Socket] -ListenStream=/tmp/live-autojoin.socket -SocketUser=www-data -SocketGroup=www-data diff --git a/live-autojoin/systemd/live-autojoin.target b/live-autojoin/systemd/live-autojoin.target deleted file mode 100644 index a028573..0000000 --- a/live-autojoin/systemd/live-autojoin.target +++ /dev/null @@ -1,5 +0,0 @@ -[Unit] -Requires=live-autojoin-cron.timer live-autojoin-service.service live-autojoin-web.socket live-autojoin-announce.service - -[Install] -WantedBy=multi-user.target -- 2.30.2