Compare commits

...

7 Commits

8 changed files with 139 additions and 18 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ config.json
planning.txt
*.pyc
/debug
lynnesbian.json

View File

@ -1,6 +1,11 @@
from bs4 import BeautifulSoup
import MySQLdb
import markovify
import requests
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from base64 import b64decode, b64encode
from mastodon import Mastodon, MastodonUnauthorizedError
import html, re, json
@ -175,3 +180,67 @@ def make_post(args):
c.execute("UPDATE bots SET last_post = CURRENT_TIMESTAMP() WHERE handle = %s", (handle,))
db.commit()
c.close()
def get_key():
db = MySQLdb.connect(
host = cfg['db_host'],
user=cfg['db_user'],
passwd=cfg['db_pass'],
db=cfg['db_name']
)
dc = db.cursor(MySQLdb.cursors.DictCursor)
dc.execute("SELECT * FROM http_auth_key")
key = dc.fetchone()
if key == None:
# generate new key
key = {}
privkey = RSA.generate(4096)
key['private'] = privkey.exportKey('PEM').decode('utf-8')
key['public'] = privkey.publickey().exportKey('PEM').decode('utf-8')
dc.execute("INSERT INTO http_auth_key (private, public) VALUES (%s, %s)", (key['private'], key['public']))
dc.close()
db.commit()
return key
def signed_get(url, timeout = 10, additional_headers = {}, request_json = True):
headers = {}
if request_json:
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
headers = {**headers, **additional_headers}
# sign request headers
key = RSA.importKey(get_key()['private'])
sigstring = ''
for header, value in headers.items():
sigstring += '{}: {}\n'.format(header.lower(), value)
sigstring.rstrip("\n")
pkcs = PKCS1_v1_5.new(key)
h = SHA256.new()
h.update(sigstring.encode('ascii'))
signed_sigstring = b64encode(pkcs.sign(h)).decode('ascii')
sig = {
'keyId': "{}/actor".format(cfg['base_uri']),
'algorithm': 'rsa-sha256',
'headers': ' '.join(headers.keys()),
'signature': signed_sigstring
}
sig_header = ['{}="{}"'.format(k, v) for k, v in sig.items()]
headers['signature'] = ','.join(sig_header)
r = requests.Request('GET', url, headers)
return r.headers
# return requests.get(url, timeout = timeout)

View File

@ -5,13 +5,16 @@ import re, json
def bot_accounts_add(mysql, cfg):
if request.method == 'POST':
# remove leading/trailing whitespace
if 'account' in request.form:
session['handle'] = request.form['account'].rstrip().lstrip()
if session['step'] == 1:
if request.form['account'] == session['bot']:
if session['handle'] == 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('@')
handle_list = session['handle'].split('@')
if len(handle_list) != 3:
# not formatted correctly
error = "Incorrectly formatted handle."
@ -19,7 +22,6 @@ def bot_accounts_add(mysql, cfg):
session['username'] = handle_list[1]
session['instance'] = handle_list[2]
session['handle'] = request.form['account']
if session['instance'] in json.load(open("blacklist.json")):
session['error'] = "Learning from accounts on this instance is not allowed."
@ -96,7 +98,7 @@ def bot_accounts_add(mysql, cfg):
return render_template("bot/accounts_add.html", error = error)
# 2. use webfinger to find user's info page
#TODO: use more reliable method
# TODO: use more reliable method
try:
uri = re.search(r'template="([^"]+)"', r.text).group(1)
uri = uri.format(uri = "{}@{}".format(session['username'], session['instance']))
@ -114,7 +116,7 @@ def bot_accounts_add(mysql, cfg):
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
# this is a link formatted like "https://instan.ce/users/username", which is what we need
uri = link['href']
found = True
break
@ -128,7 +130,7 @@ def bot_accounts_add(mysql, cfg):
if r.status_code == 200:
# success!!
c = mysql.connection.cursor()
c.execute("REPLACE INTO `fedi_accounts` (`handle`, `outbox`) VALUES (%s, %s)", (session['handle'], outbox))
c.execute("INSERT IGNORE INTO `fedi_accounts` (`handle`, `outbox`) VALUES (%s, %s)", (session['handle'], outbox))
c.execute("INSERT INTO `bot_learned_accounts` (`bot_id`, `fedi_id`) VALUES (%s, %s)", (session['bot'], session['handle']))
c.close()
mysql.connection.commit()

View File

@ -0,0 +1,21 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"
}
],
"endpoints": {},
"name": "FediBooks",
"type": "Application",
"id": "{{ base_uri }}/actor",
"manuallyApprovesFollowers": true,
"publicKey": {
"id": "{{ base_uri }}/actor#main-key",
"owner": "{{ base_uri }}/actor",
"publicKeyPem": "{{ pubkey }}"
},
"summary": "FediBooks Actor",
"preferredUsername": "fedibooks",
"url": "{{ base_uri }}/actor"
}

View File

@ -0,0 +1,13 @@
{
"aliases": [
"{{ base_uri }}/actor"
],
"links": [
{
"href": "{{ base_uri }}/actor",
"rel": "self",
"type": "application/activity+json"
}
],
"subject": "acct:fedibooks@{{ base_uri }}"
}

View File

@ -32,7 +32,11 @@ scopes_pleroma = ['read', 'write', 'push']
@app.before_request
def login_check():
if request.path not in ['/', '/about', '/welcome', '/login', '/signup', '/do/login', '/do/signup'] and not request.path.startswith("/push") and not request.path.startswith('/static'):
if request.path not in ['/', '/about', '/welcome', '/login', '/signup', '/do/login', '/do/signup'] \
and not request.path.startswith("/push") \
and not request.path.startswith('/static') \
and not request.path.startswith('/actor') \
and not request.path.startswith('/.well-known'):
# page requires authentication
if 'user_id' not in session:
return redirect(url_for('render_home'))
@ -370,9 +374,15 @@ def img_bot_generic():
def favicon():
return send_file("static/favicon.ico")
# @app.route("/.well-known/webfinger")
# def webfinger():
# return render_template("webfinger.json", base_uri = cfg['base_uri']), 200, {'Content-type':'application/json'}
@app.route("/.well-known/webfinger")
def webfinger():
return render_template("ap/webfinger.json", base_uri = cfg['base_uri']), 200, {'Content-type':'application/json'}
@app.route("/actor")
def actor():
# pubkey = functions.get_key()['public'].replace("\n", "\\n")
pubkey = functions.signed_get("https://fedi.lynnesbian.space/users/lynnesbian/outbox.json?page=true")
return render_template("ap/actor.json", base_uri = cfg['base_uri'], pubkey = pubkey), 200, {'Content-type':'application/json'}
def bot_check(bot):

View File

@ -8,13 +8,13 @@ CREATE TABLE IF NOT EXISTS `users` (
`submit` ENUM('always', 'once', 'never') DEFAULT 'once',
`generation` ENUM('always', 'once', 'never') DEFAULT 'once',
`reply` ENUM('always', 'once', 'never') DEFAULT 'once'
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `credentials` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`client_id` VARCHAR(128) NOT NULL,
`client_secret` VARCHAR(128) NOT NULL,
`secret` VARCHAR(128) NOT NULL
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `bots` (
`handle` VARCHAR(128) PRIMARY KEY,
`user_id` INT NOT NULL,
@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS `bots` (
`icon_update_time` DATETIME DEFAULT '1000-01-01 00:00:00',
FOREIGN KEY (`user_id`) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (`credentials_id`) REFERENCES credentials(id) ON DELETE CASCADE
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `fedi_accounts` (
`handle` VARCHAR(128) PRIMARY KEY,
`outbox` VARCHAR(256),
@ -45,14 +45,14 @@ CREATE TABLE IF NOT EXISTS `fedi_accounts` (
`icon` VARCHAR(512),
`icon_update_time` DATETIME DEFAULT 0,
FOREIGN KEY (`credentials_id`) REFERENCES credentials(id) ON DELETE CASCADE
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `bot_learned_accounts` (
`bot_id` VARCHAR(128) NOT NULL,
`fedi_id` VARCHAR(128) NOT NULL,
`enabled` BOOLEAN DEFAULT 1,
FOREIGN KEY (`bot_id`) REFERENCES bots(handle) ON DELETE CASCADE,
FOREIGN KEY (`fedi_id`) REFERENCES fedi_accounts(handle) ON DELETE CASCADE
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `posts` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`fedi_id` VARCHAR(128),
@ -60,14 +60,14 @@ CREATE TABLE IF NOT EXISTS `posts` (
`content` TEXT NOT NULL,
`cw` BOOLEAN NOT NULL,
FOREIGN KEY (`fedi_id`) REFERENCES fedi_accounts(handle) ON DELETE CASCADE
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `word_blacklist` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`bot_id` VARCHAR(128) NOT NULL,
`phrase` VARCHAR(128) NOT NULL,
`whole_word` BOOLEAN NOT NULL,
FOREIGN KEY (`bot_id`) REFERENCES bots(handle) ON DELETE CASCADE
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `contact_history` (
`user_id` INT NOT NULL,
`fetch` BOOLEAN DEFAULT 0,
@ -75,4 +75,8 @@ CREATE TABLE IF NOT EXISTS `contact_history` (
`generation` BOOLEAN DEFAULT 0,
`reply` BOOLEAN DEFAULT 0,
FOREIGN KEY (`user_id`) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=INNODB;
) ENGINE = INNODB;
CREATE TABLE IF NOT EXISTS `http_auth_key` (
`private` TEXT NOT NULL,
`public` TEXT NOT NULL
) ENGINE = INNODB;

View File

@ -7,4 +7,5 @@ flask-mysqldb==0.2.0
bcrypt == 3.1.7
requests==2.23.0
http-ece==1.1.0
pycryptodome==3.9.7
cryptography==2.9