2018-11-01 14:25:47 +00:00
#!/usr/bin/env python3
#Curious Greg - Curious Cat to Mastodon crossposter
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
2018-11-12 10:41:03 +00:00
#TODO: ADD RETROSPRING SUPPORT!
2018-11-12 14:16:51 +00:00
import json , hashlib , urllib , time , re , random
import requests
2018-11-01 14:25:47 +00:00
from mastodon import Mastodon
2018-11-04 23:17:28 +00:00
from flask import Flask , render_template , request , session , redirect , url_for
2018-11-10 12:56:45 +00:00
import mysql . connector
2018-11-06 10:30:48 +00:00
import bcrypt
2018-11-01 14:25:47 +00:00
2018-11-02 02:35:40 +00:00
cfg = json . load ( open ( " meta.json " ) )
2018-11-10 06:38:43 +00:00
scopes = [ " read:accounts " , " write:statuses " ]
2018-11-11 12:04:30 +00:00
settings = {
2018-11-13 14:37:59 +00:00
" cw " : True ,
2018-11-11 12:04:30 +00:00
# "disabled": False,
}
2018-11-02 02:35:40 +00:00
2018-11-10 12:56:45 +00:00
db = mysql . connector . connect ( user = cfg [ ' dbuser ' ] , password = cfg [ ' dbpass ' ] , database = cfg [ ' dbname ' ] )
2018-11-01 14:25:47 +00:00
c = db . cursor ( )
2018-11-11 12:04:30 +00:00
dc = db . cursor ( dictionary = True )
2018-11-13 02:55:18 +00:00
# +---------------------+---------------+------+-----+------------------------------------------------+-------+
# | Field | Type | Null | Key | Default | Extra |
# +---------------------+---------------+------+-----+------------------------------------------------+-------+
# | username | varchar(64) | NO | PRI | NULL | |
# | instance | varchar(128) | NO | PRI | NULL | |
# | password | tinytext | NO | | NULL | |
# | avi | text | NO | | NULL | |
# | secret | tinytext | NO | | NULL | |
# | client_id | varchar(128) | NO | | NULL | |
# | client_secret | tinytext | NO | | NULL | |
# | cc | tinytext | YES | | NULL | |
# | ccavi | varchar(128) | YES | | https://lynnesbian.space/res/ceres/cc-smol.png | |
# | latest_post | tinytext | YES | | NULL | |
2018-11-13 11:03:35 +00:00
# | last_check | int(11) | NO | | 0 | |
# | time_between_checks | int(11) | NO | | 1 | |
2018-11-13 14:37:59 +00:00
# | settings | varchar(4096) | YES | | {"cw": true} | |
2018-11-13 02:55:18 +00:00
# +---------------------+---------------+------+-----+------------------------------------------------+-------+
2018-11-13 11:03:35 +00:00
c . execute ( " CREATE TABLE IF NOT EXISTS `data` (username VARCHAR(64) NOT NULL, instance VARCHAR(128) NOT NULL, password TINYTEXT NOT NULL, avi TEXT NOT NULL, secret TINYTEXT NOT NULL, client_id VARCHAR(128) NOT NULL, client_secret TINYTEXT NOT NULL, cc TINYTEXT, ccavi VARCHAR(128) DEFAULT ' https://lynnesbian.space/res/ceres/cc-smol.png ' , latest_post TINYTEXT, last_check INT DEFAULT 0 NOT NULL, time_between_checks INT DEFAULT %s NOT NULL, settings VARCHAR(4096) DEFAULT %s , PRIMARY KEY(username, instance)) " , ( cfg [ ' min_time_between_checks ' ] , json . dumps ( settings ) , ) )
2018-11-01 15:17:02 +00:00
2018-11-02 02:59:29 +00:00
app = Flask ( cfg [ ' name ' ] )
2018-11-02 03:13:08 +00:00
app . secret_key = cfg [ ' flask_key ' ]
2018-11-02 02:59:29 +00:00
2018-11-02 03:13:08 +00:00
@app.route ( ' / ' )
def main ( ) :
2018-11-04 23:17:28 +00:00
if ' acct ' not in session :
return render_template ( " landing_page.html " )
else :
return redirect ( url_for ( ' home ' ) )
@app.route ( ' /home ' )
def home ( ) :
2018-11-06 09:11:03 +00:00
if ' acct ' in session :
2018-11-13 03:29:48 +00:00
dc . execute ( " SELECT * FROM data WHERE username = %s AND instance = %s " , ( session [ ' username ' ] , session [ ' instance ' ] ) )
2018-11-13 03:22:30 +00:00
data = dc . fetchone ( )
2018-11-13 14:04:50 +00:00
try :
for item in [ ' username ' , ' instance ' , ' avi ' , ' secret ' , ' client_id ' , ' client_secret ' , ' cc ' , ' ccavi ' ] :
session [ item ] = data [ item ]
except :
return redirect ( ' /logout ' ) #TODO: not good UX
2018-11-13 03:22:30 +00:00
2018-11-11 07:30:33 +00:00
if ' cc ' not in session :
session [ ' cc ' ] = " None "
2018-11-12 03:00:31 +00:00
if session [ ' cc ' ] == " None " or ' ccavi ' not in session :
2018-11-11 07:30:33 +00:00
#every time home is rendered without cc being set
2018-11-13 03:29:48 +00:00
c . execute ( " SELECT cc, ccavi FROM `data` WHERE client_id = %s AND instance = %s " , ( session [ ' client_id ' ] , session [ ' instance ' ] ) )
2018-11-12 03:00:31 +00:00
cc = c . fetchone ( )
if cc [ 0 ] != ' ' :
session [ ' cc ' ] = cc [ 0 ]
session [ ' ccavi ' ] = cc [ 1 ]
2018-11-11 07:30:33 +00:00
if ' last_avi_update ' not in session or session [ ' last_avi_update ' ] + ( 24 * 60 * 60 ) < time . time ( ) :
2018-11-12 01:29:09 +00:00
#avatars haven't been updated for over 24 hours, update them now
client = Mastodon ( client_id = session [ ' client_id ' ] , client_secret = session [ ' client_secret ' ] , access_token = session [ ' secret ' ] , api_base_url = session [ ' instance ' ] )
2018-11-11 07:30:33 +00:00
session [ ' avi ' ] = client . account_verify_credentials ( ) [ ' avatar ' ]
2018-11-12 07:45:54 +00:00
if session [ ' cc ' ] != " None " and session [ ' cc ' ] != None :
2018-11-11 07:30:33 +00:00
#update cc avi too
r = requests . get ( " https://curiouscat.me/api/v2/profile?username= {} " . format ( session [ ' cc ' ] ) )
j = r . json ( )
session [ ' ccavi ' ] = j [ ' userData ' ] [ ' avatar ' ]
2018-11-13 03:29:48 +00:00
c . execute ( " UPDATE data SET avi = %s , ccavi = %s WHERE client_id = %s AND instance = %s " , ( session [ ' avi ' ] , session [ ' ccavi ' ] , session [ ' client_id ' ] , session [ ' instance ' ] ) )
2018-11-11 07:30:33 +00:00
else :
2018-11-13 03:29:48 +00:00
c . execute ( " UPDATE data SET avi = %s WHERE client_id = %s AND instance = %s " , ( session [ ' avi ' ] , session [ ' client_id ' ] , session [ ' instance ' ] ) )
2018-11-12 14:30:36 +00:00
session [ ' last_avi_update ' ] = int ( time . time ( ) )
2018-11-13 14:27:26 +00:00
db . commit ( )
2018-11-12 03:00:31 +00:00
return render_template ( " home.html " , mabg = " background-image:url( ' {} ' ) " . format ( session [ ' avi ' ] ) , ccbg = " background-image:url( ' {} ' ) " . format ( session [ ' ccavi ' ] ) )
2018-11-06 09:11:03 +00:00
else :
return redirect ( url_for ( ' main ' ) )
2018-11-02 02:59:29 +00:00
2018-11-06 11:22:10 +00:00
2018-11-13 12:10:13 +00:00
@app.route ( ' /debug ' )
2018-11-06 11:22:10 +00:00
def print_debug_info ( ) :
2018-11-13 12:10:13 +00:00
if cfg [ ' debug ' ] :
return json . dumps ( session . _get_current_object ( ) )
else :
return redirect ( ' /home ' )
2018-11-06 11:22:10 +00:00
2018-11-12 07:34:35 +00:00
@app.route ( ' /logout ' )
2018-11-12 01:29:09 +00:00
def reset_session ( ) :
session . clear ( )
return redirect ( url_for ( ' main ' ) )
2018-11-06 11:22:10 +00:00
@app.route ( ' /login ' )
def log_in ( ) :
if ' acct ' in session :
#user is probably already logged in. if they aren't, home() will handle things and redirect them back here
return redirect ( url_for ( ' home ' ) )
return render_template ( " login.html " )
# return(json.dumps(client_info))
#internal stuff
2018-11-02 02:59:29 +00:00
@app.route ( ' /internal/auth_a ' )
2018-11-06 11:57:47 +00:00
def internal_auth_a ( ) : #TODO: prevent these endpoints from being spammed somehow
2018-11-04 11:36:25 +00:00
2018-11-11 07:30:33 +00:00
session [ ' instance ' ] = request . args . get ( ' instance ' , default = ' mastodon.social ' , type = str )
if not session [ ' instance ' ] . startswith ( " https:// " ) :
session [ ' instance ' ] = " https:// {} " . format ( session [ ' instance ' ] )
2018-11-06 09:11:03 +00:00
session [ ' client_id ' ] , session [ ' client_secret ' ] = Mastodon . create_app ( cfg [ ' name ' ] ,
2018-11-11 07:30:33 +00:00
api_base_url = session [ ' instance ' ] ,
2018-11-10 06:38:43 +00:00
scopes = scopes ,
2018-11-06 09:11:03 +00:00
website = cfg [ ' website ' ] ,
2018-11-13 12:08:12 +00:00
redirect_uris = [ ' {} /internal/auth_b ' . format ( cfg [ ' base_uri ' ] ) ]
2018-11-04 23:17:28 +00:00
)
2018-11-11 07:30:33 +00:00
client = Mastodon ( client_id = session [ ' client_id ' ] , client_secret = session [ ' client_secret ' ] , api_base_url = session [ ' instance ' ] )
2018-11-13 12:08:12 +00:00
url = client . auth_request_url ( client_id = session [ ' client_id ' ] , redirect_uris = ' {} /internal/auth_b ' . format ( cfg [ ' base_uri ' ] ) , scopes = scopes )
2018-11-10 06:38:43 +00:00
return redirect ( url , code = 307 )
2018-11-02 07:52:33 +00:00
2018-11-06 09:11:03 +00:00
@app.route ( ' /internal/auth_b ' )
def internal_auth_b ( ) :
#write details to DB
2018-11-11 07:30:33 +00:00
client = Mastodon ( client_id = session [ ' client_id ' ] , client_secret = session [ ' client_secret ' ] , api_base_url = session [ ' instance ' ] )
2018-11-13 12:08:12 +00:00
session [ ' secret ' ] = client . log_in ( code = request . args . get ( ' code ' ) , scopes = scopes , redirect_uri = ' {} /internal/auth_b ' . format ( cfg [ ' base_uri ' ] ) )
2018-11-10 06:38:43 +00:00
acct_info = client . account_verify_credentials ( )
session [ ' username ' ] = acct_info [ ' username ' ]
session [ ' avi ' ] = acct_info [ ' avatar ' ]
2018-11-11 07:30:33 +00:00
session [ ' acct ' ] = " @ {} @ {} " . format ( session [ ' username ' ] , session [ ' instance ' ] . replace ( " https:// " , " " ) )
2018-11-13 03:29:48 +00:00
c . execute ( " SELECT COUNT(*) FROM data WHERE username = %s AND instance = %s " , ( session [ ' username ' ] , session [ ' instance ' ] ) )
2018-11-12 01:29:09 +00:00
if c . fetchone ( ) [ 0 ] > 0 :
2018-11-06 10:30:48 +00:00
#user already has an account with CG
2018-11-11 07:30:33 +00:00
#update the user's info to use the new info we just got, then redirect them to the login page
2018-11-13 10:26:10 +00:00
c . execute ( " UPDATE data SET client_id = %s , client_secret = %s , secret = %s , avi = %s WHERE username = %s AND instance = %s " , ( session [ ' client_id ' ] , session [ ' client_secret ' ] , session [ ' secret ' ] , session [ ' avi ' ] , session [ ' username ' ] , session [ ' instance ' ] ) )
2018-11-06 10:30:48 +00:00
return redirect ( url_for ( ' log_in ' ) )
2018-11-06 11:57:47 +00:00
else :
2018-11-12 01:29:09 +00:00
return redirect ( url_for ( ' create_password ' ) )
2018-11-06 09:11:03 +00:00
2018-11-12 07:34:35 +00:00
@app.route ( ' /internal/do_login ' , methods = [ ' POST ' ] )
2018-11-06 11:22:10 +00:00
def do_login ( ) :
2018-11-11 11:31:33 +00:00
pw_in = request . form [ ' pw ' ]
2018-11-12 07:34:35 +00:00
pw_hashed = hashlib . sha256 ( pw_in . encode ( ' utf-8 ' ) ) . digest ( )
2018-11-11 11:31:33 +00:00
acct = request . form [ ' acct ' ]
2018-11-12 07:34:35 +00:00
session [ ' username ' ] = re . match ( " ^@([^@]+)@ " , acct ) . group ( 1 )
2018-11-11 11:31:33 +00:00
session [ ' instance ' ] = " https:// {} " . format ( re . search ( " @([^@]+)$ " , acct ) . group ( 1 ) )
2018-11-13 03:29:48 +00:00
dc . execute ( " SELECT * FROM data WHERE username = %s AND instance = %s " , ( session [ ' username ' ] , session [ ' instance ' ] ) )
2018-11-12 02:51:01 +00:00
data = dc . fetchone ( )
2018-11-12 07:34:35 +00:00
if bcrypt . checkpw ( pw_hashed , data [ ' password ' ] . encode ( ' utf-8 ' ) ) :
2018-11-11 12:04:30 +00:00
#password is correct, log the user in
for item in [ ' username ' , ' instance ' , ' avi ' , ' secret ' , ' client_id ' , ' client_secret ' , ' cc ' , ' ccavi ' ] :
session [ item ] = data [ item ]
2018-11-12 07:34:35 +00:00
session [ ' acct ' ] = " @ {} @ {} " . format ( session [ ' username ' ] , re . match ( " https://(.*) " , session [ ' instance ' ] ) . group ( 1 ) )
2018-11-11 12:04:30 +00:00
return redirect ( ' /home ' )
else :
return redirect ( ' /login?invalid ' )
2018-11-06 10:30:48 +00:00
2018-11-06 11:22:10 +00:00
@app.route ( ' /create_password ' )
def create_password ( ) :
2018-11-13 03:29:48 +00:00
c . execute ( " SELECT COUNT(*) FROM data WHERE username = %s AND instance = %s " , ( session [ ' username ' ] , session [ ' instance ' ] ) )
2018-11-12 02:51:01 +00:00
if c . fetchone ( ) [ 0 ] == 0 :
return render_template ( " create_password.html " , bg = " background-image:url( ' {} ' ) " . format ( session [ ' avi ' ] ) )
else :
#user already exists in database, so they already have a password
return redirect ( url_for ( ' main ' ) )
2018-11-06 11:57:47 +00:00
2018-11-06 23:10:50 +00:00
@app.route ( ' /internal/create_account ' , methods = [ ' POST ' ] )
2018-11-06 11:57:47 +00:00
def create_account ( ) :
2018-11-11 07:30:33 +00:00
pw_in = request . form [ ' pw ' ]
2018-11-12 01:29:09 +00:00
if len ( pw_in ) < 6 or pw_in == ' password ' : #TODO: this is a pretty crappy check
2018-11-11 07:30:33 +00:00
return redirect ( ' /create_password?invalid ' )
2018-11-12 01:29:09 +00:00
pw_hashed = hashlib . sha256 ( pw_in . encode ( ' utf-8 ' ) ) . digest ( )
2018-11-11 07:30:33 +00:00
pw = bcrypt . hashpw ( pw_hashed , bcrypt . gensalt ( 15 ) )
2018-11-12 01:29:09 +00:00
c . execute ( " INSERT INTO data (username, instance, avi, password, secret, client_id, client_secret) VALUES ( %s , %s , %s , %s , %s , %s , %s ) " , ( session [ ' username ' ] , session [ ' instance ' ] , session [ ' avi ' ] , pw , session [ ' secret ' ] , session [ ' client_id ' ] , session [ ' client_secret ' ] ) )
2018-11-06 11:57:47 +00:00
db . commit ( )
2018-11-11 07:30:33 +00:00
return redirect ( url_for ( ' home ' ) )
2018-11-12 08:40:50 +00:00
#cc connection
2018-11-12 08:41:43 +00:00
@app.route ( ' /cc_connect ' )
def cc_connect ( ) :
return render_template ( ' cc_connect.html ' )
2018-11-12 09:19:43 +00:00
@app.route ( ' /internal/ccc_a ' , methods = [ ' POST ' ] )
def ccc_a ( ) : #step one of curiouscat connection: retreive details
2018-11-13 11:14:57 +00:00
r = requests . get ( " https://curiouscat.me/api/v2/profile?username= {} &count=1 " . format ( request . form [ ' cc ' ] ) )
2018-11-12 09:19:43 +00:00
j = r . json ( )
if ' error ' in j :
return redirect ( ' /cc_connect?invalid ' )
session [ ' cctemp ' ] = {
" cc " : j [ ' userData ' ] [ ' username ' ] ,
" ccavi " : j [ ' userData ' ] [ ' avatar ' ] ,
2018-11-12 11:15:08 +00:00
" ccid " : j [ ' userData ' ] [ ' id ' ] ,
2018-11-13 11:14:57 +00:00
" latest_post " : j [ ' posts ' ] [ 0 ] [ ' timestamp ' ] if len ( j [ ' posts ' ] ) != 0 else 0 #only post new answers from this point onwards, rather than posting all the old ones
2018-11-12 09:19:43 +00:00
}
2018-11-12 10:41:03 +00:00
return redirect ( ' /cc_connect/confirm ' )
2018-11-12 09:19:43 +00:00
@app.route ( ' /cc_connect/confirm ' )
def cc_connect_confirm ( ) :
return render_template ( ' cc_connect_confirm.html ' , bg = " background-image:url( ' {} ' ) " . format ( session [ ' cctemp ' ] [ ' ccavi ' ] ) )
2018-11-12 10:41:03 +00:00
2018-11-12 14:16:51 +00:00
@app.route ( ' /internal/ccc_b ' ) #TODO: don't allow people to spam this
2018-11-12 10:41:03 +00:00
def ccc_b ( ) :
2018-11-12 10:58:41 +00:00
session [ ' cctemp ' ] [ ' challenge ' ] = random . randint ( 100000 , 999999 )
2018-11-12 10:41:03 +00:00
session . modified = True
form_data = {
" addressees " : session [ ' cctemp ' ] [ ' ccid ' ] ,
" anon " : " true " ,
2018-11-12 10:58:41 +00:00
" question " : " Hi {} ! Your Curious Greg authentication code is: {} . You may safely delete this question after entering the code. If you didn ' t request this, you can ignore this question. " . format ( session [ ' acct ' ] , session [ ' cctemp ' ] [ ' challenge ' ] )
2018-11-12 10:41:03 +00:00
}
r = requests . post ( " https://curiouscat.me/api/v2/post/create " , data = form_data )
j = r . json ( )
if ' success ' in j and j [ ' success ' ] == True :
return redirect ( ' /cc_connect/code ' )
else :
#todo: handle error properly
return False
@app.route ( ' /cc_connect/code ' )
def cc_connect_code ( ) :
return render_template ( ' cc_connect_code.html ' )
2018-11-12 11:15:08 +00:00
@app.route ( ' /internal/ccc_c ' , methods = [ ' POST ' ] )
def ccc_c ( ) :
2018-11-12 13:10:32 +00:00
if int ( request . form [ ' challenge ' ] ) != session [ ' cctemp ' ] [ ' challenge ' ] :
2018-11-12 11:15:08 +00:00
return redirect ( ' /cc_connect/code?invalid ' )
for item in [ ' cc ' , ' ccavi ' ] :
session [ item ] = session [ ' cctemp ' ] [ item ]
2018-11-13 11:45:25 +00:00
c . execute ( " UPDATE data SET cc = %s , ccavi = %s , latest_post = %s WHERE username = %s AND instance = %s " , ( session [ ' cc ' ] , session [ ' ccavi ' ] , session [ ' cctemp ' ] [ ' latest_post ' ] , session [ ' username ' ] , session [ ' instance ' ] ) )
2018-11-13 03:22:30 +00:00
db . commit ( )
2018-11-12 14:27:46 +00:00
del session [ ' cctemp ' ]
2018-11-12 13:10:32 +00:00
return redirect ( ' /cc_connect/complete ' )
2018-11-12 11:15:08 +00:00
@app.route ( ' /cc_connect/complete ' )
def cc_connect_complete ( ) :
return render_template ( ' cc_connect_complete.html ' , bg = " background-image:url( ' {} ' ) " . format ( session [ ' ccavi ' ] ) )
2018-11-13 12:03:57 +00:00
2018-11-13 12:14:21 +00:00
@app.route ( ' /settings ' )
2018-11-13 12:16:22 +00:00
def settings_page ( ) :
2018-11-13 12:14:21 +00:00
return render_template ( ' settings.html ' )