diff --git a/app/pages/bot/accounts_add.py b/app/pages/bot/accounts_add.py new file mode 100644 index 0000000..0fa543c --- /dev/null +++ b/app/pages/bot/accounts_add.py @@ -0,0 +1,90 @@ +from flask import session, render_template, request, redirect +import requests +from mastodon import Mastodon + +def bot_accounts_add(mysql): + if request.method == 'POST': + if session['step'] == 1: + if request.form['account'] == session['bot']: + error = "Bots cannot learn from themselves." + return render_template("bot/accounts_add.html", error = error) + + # look up user + handle_list = request.form['account'].split('@') + if len(handle_list) != 3: + # not formatted correctly + error = "Incorrectly formatted handle." + return render_template("bot/accounts_add.html", error = error) + + username = handle_list[1] + instance = handle_list[2] + + # gab check + try: + r = requests.get("https://{}/api/v1/instance".format(instance), timeout=10) + except requests.exceptions.ConnectionError: + error = "Couldn't connect to {}.".format(instance) + return render_template("bot/accounts_add.html", error = error) + except: + error = "An unknown error occurred." + return render_template("bot/accounts_add.html", error = error) + + if r.status_code == 200: + j = r.json() + if 'contact_account' in j and 'is_pro' in j['contact_account']: + # gab instance + error = "Gab instances are not supported." + return render_template("bot/accounts_add.html", error = error) + + # 1. download host-meta to find webfinger URL + r = requests.get("https://{}/.well-known/host-meta".format(instance), timeout=10) + if r.status_code != 200: + error = "Couldn't get host-meta." + return render_template("bot/accounts_add.html", error = error) + + # 2. use webfinger to find user's info page + #TODO: use more reliable method + try: + uri = re.search(r'template="([^"]+)"', r.text).group(1) + uri = uri.format(uri = "{}@{}".format(username, instance)) + except: + error = "Couldn't find WebFinger URL." + return render_template("bot/accounts_add.html", error = error) + + r = requests.get(uri, headers={"Accept": "application/json"}, timeout=10) + try: + j = r.json() + except: + error = "Invalid WebFinger response." + return render_template("bot/accounts_add.html", error = error) + + found = False + for link in j['links']: + if link['rel'] == 'self': + #this is a link formatted like "https://instan.ce/users/username", which is what we need + uri = link['href'] + found = True + break + if not found: + error = "Couldn't find a valid ActivityPub outbox URL." + return render_template("bot/accounts_add.html", error = error) + + # 3. format as outbox URL and check to make sure it works + outbox = "{}/outbox?page=true".format(uri) + r = requests.get(uri, headers={"Accept": "application/json"}, timeout=10) + if r.status_code == 200: + # success!! + c = mysql.connection.cursor() + c.execute("REPLACE INTO `fedi_accounts` (`handle`, `outbox`) VALUES (%s, %s)", (request.form['account'], outbox)) + c.execute("INSERT INTO `bot_learned_accounts` (`bot_id`, `fedi_id`) VALUES (%s, %s)", (session['bot'], request.form['account'])) + c.close() + mysql.connection.commit() + return redirect("/bot/accounts/{}".format(session['bot']), 303) + else: + error = "Couldn't access ActivityPub outbox. {} may require authenticated fetches, which FediBooks doesn't support yet.".format(instance) + return render_template("bot/accounts_add.html", error = error) + else: + # new account add request + session['step'] = 1 + + return render_template("bot/accounts_add.html", error = session.pop('error', None)) diff --git a/app/pages/bot/create.py b/app/pages/bot/create.py new file mode 100644 index 0000000..17cccc4 --- /dev/null +++ b/app/pages/bot/create.py @@ -0,0 +1,131 @@ +from flask import request, session, render_template, redirect, url_for +import requests +from mastodon import Mastodon +import re + +def bot_create(mysql, cfg, scopes, scopes_pleroma): + if request.method == 'POST': + if session['step'] == 1: + # strip leading https://, if provided + session['instance'] = re.match(r"^(?:https?:\/\/)?(.*)", request.form['instance']).group(1) + + # check for mastodon/pleroma + try: + r = requests.get("https://{}/api/v1/instance".format(session['instance']), timeout=10) + except requests.ConnectionError: + session['error'] = "Couldn't connect to https://{}.".format(session['instance']) + return render_template("bot/create.html", error = session.pop('error', None)) + except: + session['error'] = "An unknown error occurred while trying to load https://{}".format(session['instance']) + return render_template("bot/create.html", error = session.pop('error', None)) + + if r.status_code == 200: + j = r.json() + if "Pleroma" in j['version']: + session['instance_type'] = "Pleroma" + session['step'] += 1 + else: + if 'contact_account' in j and 'is_pro' in j['contact_account']: + # gab instance + session['error'] = "Gab instances are not supported." + else: + session['instance_type'] = "Mastodon" + session['step'] += 1 + + else: + # not a masto/pleroma instance + # misskey is currently unsupported + # all other instance types are also unsupported + # return an error message + #TODO: misskey + session['error'] = "Unsupported instance type. Misskey support is planned." + + elif session['step'] == 2: + # nothing needs to be done here, this step just informs the user that their instance type is supported + session['step'] += 1 + + elif session['step'] == 3: + # authenticate with the given instance and obtain credentials + if session['instance_type'] in ['Mastodon', 'Pleroma']: + redirect_uri = '{}/do/authenticate_bot'.format(cfg['base_uri']) + + session['client_id'], session['client_secret'] = Mastodon.create_app( + "FediBooks", + api_base_url="https://{}".format(session['instance']), + scopes=scopes if session['instance_type'] == 'Mastodon' else scopes_pleroma, + redirect_uris=[redirect_uri], + website=cfg['base_uri'] + ) + + client = Mastodon( + client_id=session['client_id'], + client_secret=session['client_secret'], + api_base_url="https://{}".format(session['instance']) + ) + + url = client.auth_request_url( + client_id=session['client_id'], + redirect_uris=redirect_uri, + scopes=scopes if session['instance_type'] == 'Mastodon' else scopes_pleroma + ) + return redirect(url, code=303) + + elif session['instance_type'] == 'Misskey': + # todo + pass + + else: + # the user clicked next on step 2 while having an unsupported instance type + # take them back home + del session['instance'] + del session['instance_type'] + session['step'] = 1 + return redirect(url_for("home"), 303) + + else: + if 'step' in session and session['step'] == 4: + try: + # test authentication + client = Mastodon(client_id=session['client_id'], client_secret=session['client_secret'], api_base_url=session['instance']) + session['secret'] = client.log_in( + code = session['code'], + scopes=scopes if session['instance_type'] == 'Mastodon' else scopes_pleroma, + redirect_uri='{}/do/authenticate_bot'.format(cfg['base_uri']) + ) + username = client.account_verify_credentials()['username'] + handle = "@{}@{}".format(username, session['instance']) + except: + # authentication error occurred + error = "Authentication failed." + session['step'] = 3 + return render_template("bot/create.html", error = error) + + # authentication success!! + c = mysql.connection.cursor() + c.execute("INSERT INTO `credentials` (client_id, client_secret, secret) VALUES (%s, %s, %s)", (session['client_id'], session['client_secret'], session['secret'])) + credentials_id = c.lastrowid + mysql.connection.commit() + + # get webpush url + privated, publicd = client.push_subscription_generate_keys() + private = privated['privkey'] + public = publicd['pubkey'] + secret = privated['auth'] + client.push_subscription_set("{}/push/{}".format(cfg['base_uri'], handle), publicd, mention_events = True) + + c.execute("INSERT INTO `bots` (handle, user_id, credentials_id, push_public_key, push_private_key, push_secret, instance_type) VALUES (%s, %s, %s, %s, %s, %s, %s)", (handle, session['user_id'], credentials_id, public, private, secret, session['instance_type'])) + mysql.connection.commit() + c.close() + + # clean up unneeded variables + del session['code'] + del session['instance'] + del session['instance_type'] + del session['client_id'] + del session['client_secret'] + else: + # user is starting a new bot create request + session['step'] = 1 + + + return render_template("bot/create.html", error = session.pop('error', None)) diff --git a/app/webui.py b/app/webui.py index 8c57153..6112789 100644 --- a/app/webui.py +++ b/app/webui.py @@ -12,6 +12,8 @@ import functions from pages.home import home from pages.settings import settings from pages.bot.edit import bot_edit +from pages.bot.accounts_add import accounts_add +from pages.bot.create import create cfg = json.load(open("config.json")) @@ -124,92 +126,8 @@ def bot_accounts(id): return render_template("bot/accounts.html", users = users, post_count = post_count) @app.route("/bot/accounts/add", methods = ['GET', 'POST']) -def bot_accounts_add(): - if request.method == 'POST': - if session['step'] == 1: - if request.form['account'] == session['bot']: - error = "Bots cannot learn from themselves." - return render_template("bot/accounts_add.html", error = error) - - # look up user - handle_list = request.form['account'].split('@') - if len(handle_list) != 3: - # not formatted correctly - error = "Incorrectly formatted handle." - return render_template("bot/accounts_add.html", error = error) - - username = handle_list[1] - instance = handle_list[2] - - # gab check - try: - r = requests.get("https://{}/api/v1/instance".format(instance), timeout=10) - except requests.exceptions.ConnectionError: - error = "Couldn't connect to {}.".format(instance) - return render_template("bot/accounts_add.html", error = error) - except: - error = "An unknown error occurred." - return render_template("bot/accounts_add.html", error = error) - - if r.status_code == 200: - j = r.json() - if 'contact_account' in j and 'is_pro' in j['contact_account']: - # gab instance - error = "Gab instances are not supported." - return render_template("bot/accounts_add.html", error = error) - - # 1. download host-meta to find webfinger URL - r = requests.get("https://{}/.well-known/host-meta".format(instance), timeout=10) - if r.status_code != 200: - error = "Couldn't get host-meta." - return render_template("bot/accounts_add.html", error = error) - - # 2. use webfinger to find user's info page - #TODO: use more reliable method - try: - uri = re.search(r'template="([^"]+)"', r.text).group(1) - uri = uri.format(uri = "{}@{}".format(username, instance)) - except: - error = "Couldn't find WebFinger URL." - return render_template("bot/accounts_add.html", error = error) - - r = requests.get(uri, headers={"Accept": "application/json"}, timeout=10) - try: - j = r.json() - except: - error = "Invalid WebFinger response." - return render_template("bot/accounts_add.html", error = error) - - found = False - for link in j['links']: - if link['rel'] == 'self': - #this is a link formatted like "https://instan.ce/users/username", which is what we need - uri = link['href'] - found = True - break - if not found: - error = "Couldn't find a valid ActivityPub outbox URL." - return render_template("bot/accounts_add.html", error = error) - - # 3. format as outbox URL and check to make sure it works - outbox = "{}/outbox?page=true".format(uri) - r = requests.get(uri, headers={"Accept": "application/json"}, timeout=10) - if r.status_code == 200: - # success!! - c = mysql.connection.cursor() - c.execute("REPLACE INTO `fedi_accounts` (`handle`, `outbox`) VALUES (%s, %s)", (request.form['account'], outbox)) - c.execute("INSERT INTO `bot_learned_accounts` (`bot_id`, `fedi_id`) VALUES (%s, %s)", (session['bot'], request.form['account'])) - c.close() - mysql.connection.commit() - return redirect("/bot/accounts/{}".format(session['bot']), 303) - else: - error = "Couldn't access ActivityPub outbox. {} may require authenticated fetches, which FediBooks doesn't support yet.".format(instance) - return render_template("bot/accounts_add.html", error = error) - else: - # new account add request - session['step'] = 1 - - return render_template("bot/accounts_add.html", error = session.pop('error', None)) +def render_bot_accounts_add(): + return bot_accounts_add(mysql) @app.route("/bot/accounts/toggle/") def bot_accounts_toggle(id): @@ -239,132 +157,8 @@ def bot_accounts_delete(id): return redirect("/bot/accounts/{}".format(session['bot']), 303) @app.route("/bot/create/", methods=['GET', 'POST']) -def bot_create(): - if request.method == 'POST': - if session['step'] == 1: - # strip leading https://, if provided - session['instance'] = re.match(r"^(?:https?:\/\/)?(.*)", request.form['instance']).group(1) - - # check for mastodon/pleroma - try: - r = requests.get("https://{}/api/v1/instance".format(session['instance']), timeout=10) - except requests.ConnectionError: - session['error'] = "Couldn't connect to https://{}.".format(session['instance']) - return render_template("bot/create.html", error = session.pop('error', None)) - except: - session['error'] = "An unknown error occurred while trying to load https://{}".format(session['instance']) - return render_template("bot/create.html", error = session.pop('error', None)) - - if r.status_code == 200: - j = r.json() - if "Pleroma" in j['version']: - session['instance_type'] = "Pleroma" - session['step'] += 1 - else: - if 'contact_account' in j and 'is_pro' in j['contact_account']: - # gab instance - session['error'] = "Gab instances are not supported." - else: - session['instance_type'] = "Mastodon" - session['step'] += 1 - - else: - # not a masto/pleroma instance - # misskey is currently unsupported - # all other instance types are also unsupported - # return an error message - #TODO: misskey - session['error'] = "Unsupported instance type. Misskey support is planned." - - elif session['step'] == 2: - # nothing needs to be done here, this step just informs the user that their instance type is supported - session['step'] += 1 - - elif session['step'] == 3: - # authenticate with the given instance and obtain credentials - if session['instance_type'] in ['Mastodon', 'Pleroma']: - redirect_uri = '{}/do/authenticate_bot'.format(cfg['base_uri']) - - session['client_id'], session['client_secret'] = Mastodon.create_app( - "FediBooks", - api_base_url="https://{}".format(session['instance']), - scopes=scopes if session['instance_type'] == 'Mastodon' else scopes_pleroma, - redirect_uris=[redirect_uri], - website=cfg['base_uri'] - ) - - client = Mastodon( - client_id=session['client_id'], - client_secret=session['client_secret'], - api_base_url="https://{}".format(session['instance']) - ) - - url = client.auth_request_url( - client_id=session['client_id'], - redirect_uris=redirect_uri, - scopes=scopes if session['instance_type'] == 'Mastodon' else scopes_pleroma - ) - return redirect(url, code=303) - - elif session['instance_type'] == 'Misskey': - # todo - pass - - else: - # the user clicked next on step 2 while having an unsupported instance type - # take them back home - del session['instance'] - del session['instance_type'] - session['step'] = 1 - return redirect(url_for("home"), 303) - - else: - if 'step' in session and session['step'] == 4: - try: - # test authentication - client = Mastodon(client_id=session['client_id'], client_secret=session['client_secret'], api_base_url=session['instance']) - session['secret'] = client.log_in( - code = session['code'], - scopes=scopes if session['instance_type'] == 'Mastodon' else scopes_pleroma, - redirect_uri='{}/do/authenticate_bot'.format(cfg['base_uri']) - ) - username = client.account_verify_credentials()['username'] - handle = "@{}@{}".format(username, session['instance']) - except: - # authentication error occurred - error = "Authentication failed." - session['step'] = 3 - return render_template("bot/create.html", error = error) - - # authentication success!! - c = mysql.connection.cursor() - c.execute("INSERT INTO `credentials` (client_id, client_secret, secret) VALUES (%s, %s, %s)", (session['client_id'], session['client_secret'], session['secret'])) - credentials_id = c.lastrowid - mysql.connection.commit() - - # get webpush url - privated, publicd = client.push_subscription_generate_keys() - private = privated['privkey'] - public = publicd['pubkey'] - secret = privated['auth'] - client.push_subscription_set("{}/push/{}".format(cfg['base_uri'], handle), publicd, mention_events = True) - - c.execute("INSERT INTO `bots` (handle, user_id, credentials_id, push_public_key, push_private_key, push_secret, instance_type) VALUES (%s, %s, %s, %s, %s, %s, %s)", (handle, session['user_id'], credentials_id, public, private, secret, session['instance_type'])) - mysql.connection.commit() - c.close() - - # clean up unneeded variables - del session['code'] - del session['instance'] - del session['instance_type'] - del session['client_id'] - del session['client_secret'] - else: - # user is starting a new bot create request - session['step'] = 1 - - - return render_template("bot/create.html", error = session.pop('error', None)) +def render_bot_create(): + return bot_create(mysql, cfg, scopes, scopes_pleroma) @app.route("/bot/create/back") def bot_create_back():