#!/usr/bin/env python3 import os import base64 import bottle import sqlite3 import sendmail import backend import jwt import pylibscrypt import smtplib import tweepy from mastodon import Mastodon # from bottle_auth import AuthPlugin class Datagetter(object): def __init__(self): self.db = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "ticketfrei.sqlite") self.conn = self.create_connection(self.db) self.cur = self.conn.cursor() def create_connection(self, db_file): """ create a database connection to the SQLite database specified by the db_file :param db_file: database file :return: Connection object or None """ try: conn = sqlite3.connect(db_file) return conn except sqlite3.Error as e: print(e) return None app = application = bottle.Bottle() @app.route('/login', method="POST") def login(): """ Login to the ticketfrei account with credentials from the user table. :return: bot.py Session Cookie """ email = bottle.request.forms.get('uname') psw = bottle.request.forms.get('psw') psw = psw.encode("utf-8") db.cur.execute("SELECT pass_hashed FROM user WHERE email=?;", (email, )) try: pass_hashed = db.cur.fetchone()[0] except TypeError: return "Wrong Credentials." # no user with this email if pylibscrypt.scrypt_mcf_check(pass_hashed, psw): bottle.response.set_cookie("account", email, secret) return bottle.redirect("/settings") else: return "Wrong Credentials." # passphrase is wrong @app.route('/register', method="POST") def register(): """ Login to the ticketfrei account with credentials from the user table. :return: bot.py Session Cookie """ email = bottle.request.forms.get('email') psw = bottle.request.forms.get('psw') pswrepeat = bottle.request.forms.get('psw-repeat') if pswrepeat != psw: return "ERROR: Passwords don't match. Try again." # check if email is already in use db.cur.execute("SELECT id FROM user WHERE email=?;", (email,)) if db.cur.fetchone() is not None: return "E-Mail is already in use." # account already exists # hash and format for being encoded in the confirmation mail psw = psw.encode("utf-8") pass_hashed = pylibscrypt.scrypt_mcf(psw) # hash password pass_hashed = base64.encodebytes(pass_hashed) pass_hashed = pass_hashed.decode("ascii") payload = {"email": email, "pass_hashed": pass_hashed} # create confirm_link encoded_jwt = jwt.encode(payload, secret).decode('utf-8') confirm_link = "http://" + bottle.request.get_header('host') + "/confirm/" + str(encoded_jwt) # :todo http -> https # send the mail m = sendmail.Mailer(config) try: m.send("Complete your registration here: " + confirm_link, email, "[Ticketfrei] Confirm your account") except smtplib.SMTPRecipientsRefused: return "Please enter a valid E-Mail address." return "We sent you an E-Mail. Please click on the confirmation link." @app.route('/confirm/', method="GET") def confirm_account(encoded_jwt): """ Confirm the account creation and create a database entry. :return: Redirection to bot.html """ # get values from URL payload = jwt.decode(encoded_jwt, secret) email = payload["email"] pass_hashed = base64.b64decode(payload["pass_hashed"]) # create db entry db.cur.execute("INSERT INTO user(email, pass_hashed, enabled) VALUES(?, ?, ?);", (email, pass_hashed, 1)) # insert default good- & blacklist into db with open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "goodlists", "nbg_goodlist"), "r") as f: default_goodlist = f.read() db.cur.execute("INSERT INTO trigger_good(user_id, words) VALUES(?, ?);", (get_user_id(email), default_goodlist)) with open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "blacklists", "nbg_blacklist"), "r") as f: default_blacklist = f.read() db.cur.execute("INSERT INTO trigger_bad(user_id, words) VALUES(?, ?);", (get_user_id(email), default_blacklist)) db.conn.commit() bottle.response.set_cookie("account", email, secret, path="/") return bottle.redirect("/settings") @app.route('/settings') def manage_bot(): """ Restricted area. Deliver the bot settings page. Deliver user settings with Cookies. :return: If it returns something, it just refreshes the page. """ email = bottle.request.get_cookie("account", secret=secret) if email is not None: user_id = get_user_id(email) # get Enable Status from db db.cur.execute("SELECT enabled FROM user WHERE email = ?;", (email,)) enabled = db.cur.fetchone()[0] # Set Enable Status with a Cookie resp = bottle.static_file("../static/bot.html", root='../static') if enabled: resp.set_cookie("enabled", "True") else: resp.set_cookie("enabled", "False") # Get goodlist from db db.cur.execute("SELECT words FROM trigger_good WHERE user_id=?;", (user_id,)) words = db.cur.fetchone()[0] # Deliver goodlist with a Cookie resp.set_cookie("goodlist", words, path="/settings") # Get blacklist from db db.cur.execute("SELECT words FROM trigger_bad WHERE user_id=?;", (user_id,)) words = db.cur.fetchone()[0] # Deliver badlist with a Cookie resp.set_cookie("blacklist", words, path="/settings") return resp else: bottle.abort(401, "Wrong username or passphrase. Try again!") def get_user_id(email): # get user_id from email db.cur.execute("SELECT id FROM user WHERE email = ?", (email, )) return db.cur.fetchone()[0] @app.route('/settings/goodlist', method="POST") def update_goodlist(): """ Writes the goodlist textarea on /settings to the database. This function expects a multi-line string, transmitted over the textarea form. :return: redirect to settings page """ # get new goodlist words = bottle.request.forms.get("goodlist") user_id = get_user_id(bottle.request.get_cookie("account", secret=secret)) # write new goodlist to db db.cur.execute("UPDATE trigger_good SET words = ? WHERE user_id = ?;", (words, user_id, )) db.conn.commit() return bottle.redirect("/settings") @app.route('/settings/blacklist', method="POST") def update_blacklist(): """ Writes the blacklist textarea on /settings to the database. This function expects a multi-line string, transmitted over the textarea form. :return: redirect to settings page """ # get new blacklist words = bottle.request.forms.get("blacklist") # get user_id user_id = get_user_id(bottle.request.get_cookie("account", secret=secret)) # write new goodlist to db db.cur.execute("UPDATE trigger_bad SET words = ? WHERE user_id = ?;", (words, user_id, )) db.conn.commit() return bottle.redirect("/settings") @app.route('/enable', method="POST") def enable(): """ Enable the bot. Called by the Enable button in bot.html :return: redirect to settings page """ email = bottle.request.get_cookie("account", secret=secret) db.cur.execute("UPDATE user SET enabled = 1 WHERE email=?;", (email,)) db.conn.commit() bottle.response.set_cookie("enabled", "True") return bottle.redirect("/settings") @app.route('/disable', method="POST") def disable(): """ Disable the bot. Called by the Disable button in bot.html :return: redirect to settings page """ email = bottle.request.get_cookie("account", secret=secret) db.cur.execute("UPDATE user SET enabled = 0 WHERE email=?;", (email,)) db.conn.commit() bottle.response.set_cookie("enabled", "False") return bottle.redirect("/settings") @app.route('/login/twitter') def login_twitter(): """ Starts the twitter OAuth authentication process. :return: redirect to twitter. """ email = bottle.request.get_cookie("account", secret=secret) consumer_key = config["tapp"]["consumer_key"] consumer_secret = config["tapp"]["consumer_secret"] callback_url = host + "/login/twitter/callback" auth = tweepy.OAuthHandler(consumer_key, consumer_secret, callback_url) try: redirect_url = auth.get_authorization_url() except tweepy.TweepError: return 'Error! Failed to get request token.' db.cur.execute("INSERT INTO twitter_request_tokens(user_id, request_token) VALUES(?, ?);", (get_user_id(email), auth.request_token)) db.conn.commit() return bottle.redirect(redirect_url) @app.route('/login/twitter/callback') def twitter_callback(): """ Gets the callback :return: """ # twitter passes the verifier/oauth token secret in a GET request. verifier = bottle.request.query('oauth_verifier') email = bottle.request.get_cookie("account", secret=secret) consumer_key = config["tapp"]["consumer_key"] consumer_secret = config["tapp"]["consumer_secret"] auth = tweepy.OAuthHandler(consumer_key, consumer_secret) db.cur.execute("SELECT request_token FROM twitter_request_tokens WHERE user_id = ?;", (get_user_id(email))) try: request_token = db.cur.fetchone()[0] except ValueError: return "Could not get request token from db." db.cur.execute("DELETE FROM twitter_request_tokens WHERE user_id = ?;", (get_user_id(email))) db.conn.commit() auth.request_token = { "oauth_token" : request_token, "oauth_token_secret" : verifier} try: auth.get_access_token(verifier) except tweepy.TweepError: print('Error! Failed to get access token.') db.cur.execute("INSERT INTO twitter_accounts(user_id, access_token_key, access_token_secret) VALUES(?, ?, ?);", (get_user_id(email), auth.access_token, auth.access_token_secret)) db.conn.commit() return bottle.redirect("/settings") @app.route('/login/mastodon', method="POST") def login_mastodon(): """ Starts the mastodon OAuth authentication process. :return: redirect to twitter. """ email = bottle.request.get_cookie("account", secret=secret) # get app tokens instance_url = bottle.request.forms.get('instance_url') db.cur.execute("SELECT client_id, client_secret FROM mastodon_instances WHERE instance = ?;", (instance_url)) try: client_id, client_secret = db.cur.fetchone()[0] except TypeError: id = "ticketfrei" + str(secret)[0:4] client_id, client_secret = Mastodon.create_app(id, api_base_url=instance_url) db.cur.execute("INSERT INTO mastodon_instances(instance, client_id, client_secret) VALUES(?, ?, ?);", (instance_url, client_id, client_secret)) db.conn.commit() # get access token and write it to db uname = bottle.request.forms.get('uname') psw = bottle.request.forms.get('psw') mastodon = Mastodon( client_id=client_id, client_secret=client_secret, api_base_url=instance_url ) access_token = mastodon.log_in(uname, psw) db.cur.execute("SELECT id FROM mastodon_instances WHERE instance = ?;", (instance_url)) instance_id = db.cur.fetchone()[0] db.cur.execute("INSERT INTO mastodon_accounts(user_id, access_token, instance_id, active) VALUES(?, ?, ?, ?);", (get_user_id(email), access_token, instance_id, 1)) return bottle.redirect("/settings") @app.route('/static/') def static(filename): """ Serve static files """ if filename == "bot.html": bottle.abort(401, "Sorry, access denied.") return bottle.static_file(filename, root='../static') @app.route('/') def show_index(): """ The front "index" page :return: /static/index.html """ return bottle.static_file("../static/index.html", root='../static') class StripPathMiddleware(object): """ Get that slash out of the request """ def __init__(self, a): self.a = a def __call__(self, e, h): e['PATH_INFO'] = e['PATH_INFO'].rstrip('/') return self.a(e, h) if __name__ == "__main__": global config config = backend.get_config() global db global secret global twitter secret = os.urandom(32) db = Datagetter() host = '0.0.0.0' # from bottle_auth.social import twitter as twitterplugin # callback_url = host + '/login/twitter/callback' # twitter = twitterplugin.Twitter(config['tapp']['consumer_key'], config['tapp']['consumer_secret'], callback_url) # bottle.install(AuthPlugin(twitter)) try: bottle.run(app=StripPathMiddleware(app), host=host, port=8080) finally: db.conn.close()