1
0
Fork 0
mirror of https://github.com/Lynnesbian/FediBooks/ synced 2024-11-25 16:48:58 +00:00

Compare commits

...

23 commits

Author SHA1 Message Date
2cbad2eff3 truly fixed the weird bug. requesting / no longer resets session['step'] 2019-09-10 17:06:24 +10:00
28ca5c3d83 add favicon to fix a very weird bug 2019-09-10 17:00:14 +10:00
fb1f05d96b redirect to the home page on invalid instance type 2019-09-10 16:21:00 +10:00
44c9cb0248 remove unecessary form actions 2019-09-10 16:10:58 +10:00
bd5733ef11 add wsgi.py for use with gunicorn 2019-09-10 15:08:48 +10:00
313dbc37fd make login and signup pages look better 2019-09-10 15:06:31 +10:00
b39a10c16f remove erroneous close bracket 2019-09-10 14:41:16 +10:00
bc6745ae02 use service_threads instead of hardcoding 8 2019-09-10 14:22:31 +10:00
4de3997b5a add sample config 2019-09-10 14:21:43 +10:00
97bfa28694 prepare for initial release 2019-09-10 14:18:21 +10:00
787851b13a bot settings page is now fully functional! 2019-09-10 14:07:44 +10:00
6838e16b03 bot_edit now displays user's settings 2019-09-10 13:36:36 +10:00
efa1eef5cc hide report error button until email works 2019-09-10 13:14:48 +10:00
9af94124b6 correctly format an error 2019-09-10 13:13:06 +10:00
43a1ff62b6 handle errors when adding accounts 2019-09-10 13:12:40 +10:00
c9da5d9f40 block certain instance types in accounts_add 2019-09-10 13:03:28 +10:00
d6b43fa6d4 update footer 2019-09-10 12:26:16 +10:00
91c732ca94 update about page 2019-09-10 12:22:42 +10:00
a5706ccd59 deleted one blank line uwu 2019-09-10 12:09:38 +10:00
f2d15b7d80 moved scraping functions to a separate file 2019-09-10 12:07:53 +10:00
989a9ec98b implement wait between posts 2019-09-10 11:50:55 +10:00
fb017468fd less inflammatory error message 2019-09-10 11:41:27 +10:00
d7f15601d5 implement email verification in db 2019-09-10 11:17:06 +10:00
17 changed files with 297 additions and 135 deletions

9
config.sample.json Normal file
View file

@ -0,0 +1,9 @@
{
"secret_key": "enter a random string here",
"db_name": "fedibooks",
"db_user": "fedibooks",
"db_pass": "enter your database password here",
"db_host": "localhost",
"base_uri": "enter your base URI here, e.g. https://example.com",
"service_threads": 8
}

View file

@ -124,4 +124,5 @@ def make_post(handle):
print(post)
client.status_post(post, visibility = bot['post_privacy'], spoiler_text = bot['content_warning'])
# TODO: update date of last post
c.execute("UPDATE bots SET last_post = CURRENT_TIMESTAMP() WHERE handle = %s", (handle,))
db.commit()

100
scrape.py Normal file
View file

@ -0,0 +1,100 @@
import MySQLdb
import requests
from multiprocessing import Pool
import json, re
import functions
cfg = json.load(open('config.json'))
def scrape_posts(account):
handle = account[0]
outbox = account[1]
print("Scraping {}".format(handle))
c = db.cursor()
last_post = 0
c.execute("SELECT COUNT(*) FROM `posts` WHERE `fedi_id` = %s", (handle,))
if c.fetchone()[0] > 0:
# we've downloaded this user's posts before
# find out the most recently downloaded post of theirs
c.execute("SELECT `post_id` FROM `posts` WHERE `fedi_id` = %s ORDER BY `id` DESC LIMIT 1", (handle,))
last_post = c.fetchone()[0]
r = requests.get(outbox)
j = r.json()
# check for pleroma
pleroma = 'next' not in j
if pleroma:
j = j['first']
else:
uri = "{}&min_id={}".format(outbox, last_post)
r = requests.get(uri)
j = r.json()
# here we go!
# warning: scraping posts from outbox.json is messy stuff
done = False
while not done and len(j['orderedItems']) > 0:
for oi in j['orderedItems']:
if oi['type'] == "Create":
# this is a status/post/toot/florp/whatever
# first, check to see if we already have this in the database
post_id = re.search(r"([^\/]+)/?$", oi['object']['id']).group(1) # extract 123 from https://example.com/posts/123/
c.execute("SELECT COUNT(*) FROM `posts` WHERE `fedi_id` = %s AND `post_id` = %s", (handle, post_id))
if c.fetchone()[0] > 0:
# this post is already in the DB.
# we'll set done to true because we've caught up to where we were last time.
done = True
# we'll still iterate over the rest of the posts, though, in case there are still some new ones on this page.
continue
content = oi['object']['content']
# remove HTML tags and such from post
content = functions.extract_post(content)
if len(content) > 65535:
# post is too long to go into the DB
continue
try:
c.execute("INSERT INTO `posts` (`fedi_id`, `post_id`, `content`, `cw`) VALUES (%s, %s, %s, %s)", (
handle,
post_id,
content,
1 if (oi['object']['summary'] != None and oi['object']['summary'] != "") else 0
))
except:
#TODO: error handling
raise
if not done:
if pleroma:
r = requests.get(j['next'], timeout = 10)
else:
r = requests.get(j['prev'], timeout = 10)
if r.status_code == 429:
# we are now being ratelimited, move on to the next user
done = True
else:
j = r.json()
db.commit()
c.close()
print("Establishing DB connection")
db = MySQLdb.connect(
host = cfg['db_host'],
user=cfg['db_user'],
passwd=cfg['db_pass'],
db=cfg['db_name']
)
cursor = db.cursor()
print("Downloading posts")
cursor.execute("SELECT `handle`, `outbox` FROM `fedi_accounts` ORDER BY RAND()")
accounts = cursor.fetchall()
with Pool(cfg['service_threads']) as p:
p.map(scrape_posts, accounts)
print("Done!")

View file

@ -1,89 +1,11 @@
#!/usr/bin/env python3
from mastodon import Mastodon
import MySQLdb
import requests
import markovify
from multiprocessing import Pool
import json, re
import json
import functions
cfg = json.load(open('config.json'))
def scrape_posts(account):
handle = account[0]
outbox = account[1]
print("Scraping {}".format(handle))
c = db.cursor()
last_post = 0
c.execute("SELECT COUNT(*) FROM `posts` WHERE `fedi_id` = %s", (handle,))
if c.fetchone()[0] > 0:
# we've downloaded this user's posts before
# find out the most recently downloaded post of theirs
c.execute("SELECT `post_id` FROM `posts` WHERE `fedi_id` = %s ORDER BY `id` DESC LIMIT 1", (handle,))
last_post = c.fetchone()[0]
r = requests.get(outbox)
j = r.json()
# check for pleroma
pleroma = 'next' not in j
if pleroma:
j = j['first']
else:
uri = "{}&min_id={}".format(outbox, last_post)
r = requests.get(uri)
j = r.json()
# here we go!
# warning: scraping posts from outbox.json is messy stuff
done = False
while not done and len(j['orderedItems']) > 0:
for oi in j['orderedItems']:
if oi['type'] == "Create":
# this is a status/post/toot/florp/whatever
# first, check to see if we already have this in the database
post_id = re.search(r"([^\/]+)/?$", oi['object']['id']).group(1) # extract 123 from https://example.com/posts/123/
c.execute("SELECT COUNT(*) FROM `posts` WHERE `fedi_id` = %s AND `post_id` = %s", (handle, post_id))
if c.fetchone()[0] > 0:
# this post is already in the DB.
# we'll set done to true because we've caught up to where we were last time.
done = True
# we'll still iterate over the rest of the posts, though, in case there are still some new ones on this page.
continue
content = oi['object']['content']
# remove HTML tags and such from post
content = functions.extract_post(content)
if len(content) > 65535:
# post is too long to go into the DB
continue
try:
c.execute("INSERT INTO `posts` (`fedi_id`, `post_id`, `content`, `cw`) VALUES (%s, %s, %s, %s)", (
handle,
post_id,
content,
1 if (oi['object']['summary'] != None and oi['object']['summary'] != "") else 0
))
except:
#TODO: error handling
raise
if not done:
if pleroma:
r = requests.get(j['next'], timeout = 10)
else:
r = requests.get(j['prev'], timeout = 10)
if r.status_code == 429:
# we are now being ratelimited, move on to the next user
done = True
else:
j = r.json()
db.commit()
c.close()
print("Establishing DB connection")
db = MySQLdb.connect(
host = cfg['db_host'],
@ -95,19 +17,13 @@ db = MySQLdb.connect(
print("Cleaning up database")
# delete any fedi accounts we no longer need
cursor = db.cursor()
cursor.execute("DELETE FROM fedi_accounts WHERE handle NOT IN (SELECT fedi_id FROM bot_learned_accounts);")
print("Downloading posts")
cursor.execute("SELECT `handle`, `outbox` FROM `fedi_accounts` ORDER BY RAND()")
accounts = cursor.fetchall()
# with Pool(8) as p:
# p.map(scrape_posts, accounts)
cursor.execute("DELETE FROM fedi_accounts WHERE handle NOT IN (SELECT fedi_id FROM bot_learned_accounts)")
print("Generating posts")
cursor.execute("SELECT handle FROM bots WHERE enabled = TRUE")
cursor.execute("SELECT handle FROM bots WHERE enabled = TRUE AND TIMESTAMPDIFF(MINUTE, last_post, CURRENT_TIMESTAMP()) > post_frequency")
bots = cursor.fetchall()
with Pool(8) as p:
with Pool(cfg['service_threads']) as p:
p.map(functions.make_post, bots)
#TODO: other cron tasks should be done here, like updating profile pictures

View file

@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`email` VARCHAR(128) UNIQUE NOT NULL,
`password` BINARY(60) NOT NULL,
`email_verified` BOOLEAN DEFAULT 0,
`fetch` ENUM('always', 'once', 'never') DEFAULT 'once',
`submit` ENUM('always', 'once', 'never') DEFAULT 'once',
`generation` ENUM('always', 'once', 'never') DEFAULT 'once',
@ -20,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `bots` (
`credentials_id` INT NOT NULL,
`push_private_key` BINARY(128) NOT NULL,
`push_public_key` BINARY(128) NOT NULL,
`push_secret` BINARY(16)),
`push_secret` BINARY(16),
`enabled` BOOLEAN DEFAULT 0,
`replies_enabled` BOOLEAN DEFAULT 1,
`post_frequency` SMALLINT UNSIGNED DEFAULT 30,

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View file

@ -47,6 +47,9 @@ body {
.no-margin {
margin: 0;
}
.left-align {
text-align: left;
}
.panel-icon {
width: 100px;
@ -135,6 +138,10 @@ a:visited {
color: mediumpurple;
}
p {
line-height: 1.4em;
}
h1 {
font-size: 4em;
margin-top: 10px !important;
@ -165,8 +172,13 @@ input:focus, select:focus, textarea:focus {
border: 3px mediumpurple solid;
}
input[type="checkbox"] {
height: 1.4em;
}
label, input {
flex-basis: 0;
text-align: left;
}
label {
flex-grow: 1;

View file

@ -23,15 +23,16 @@
<h2 class="thin">Why create FediBooks?</h2>
<p>I've been working on ebooks bots for a long time. My first project, <a href="https://github.com/Lynnesbian/mstdn-ebooks/">mstdn-ebooks</a>, dates back to October 2018. It's a much less ambitious project than FediBooks, but maintaining and developing it has still been a formidable undertaking. It's changed a lot since the initial version, and has worked very well for a long time.</p>
<p>Using mstdn-ebooks is nowhere near as easy as using FediBooks. There's <a href="https://cloud.lynnesbian.space/s/jozbRi69t4TpD95">a long guide</a> you'll need to follow, and you'll have to install some programs like Python and possibly Git to get working. It runs on your own computer, meaning that if you put it into sleep mode, it stops working. Updating it to the latest version is also your responsibility, and the error messages it gives aren't exactly user friendly.</p>
<p>To help with this issue, I decided to create a Patreon where I offered a $2.50 per month tier to host up to three bots for you. I would take care of configuration, maintenance, updating, et cetera. This worked pretty well up until recently.</p>
<p>A new change in Mastodon and Pleroma adds something called <em>authenticated fetches</em>. Reffered to as "authorised mode" by Mastodon, this new feature prevents unauthorised instances and programs from accessing the API. In other words, if instance A blocks instance B, then instance B won't be able to see instance A's public posts anymore. However, this also means mstdn-ebooks can't see your posts. This can be fixed, however, by requesting permission from instance B. So what's the problem?</p>
<p>Using mstdn-ebooks is nowhere near as easy as using FediBooks. There's <a href="https://cloud.lynnesbian.space/s/jozbRi69t4TpD95">a long guide</a> you'll need to follow, and you'll have to install some programs like Python and possibly Git to get it working. It runs on your own computer, meaning that if you put it into sleep mode or disconnect from the internet, it stops working. Updating mstdn-ebooks to the latest version is also your responsibility, and the <a href="https://github.com/Lynnesbian/mstdn-ebooks/blob/910829d9464a9ef5f0365b4df3e57e47d166a7d8/main.py#L191">error messages it gives</a> aren't exactly user friendly.</p>
<p>To help with these issues, I decided to create a Patreon where I offered a $2.50 per month tier to host up to three bots for you. I would take care of configuration, maintenance, updating, et cetera. This worked pretty well up until recently.</p>
<p>A recent change in Mastodon and Pleroma adds something called <em>authenticated fetches</em>. Reffered to as "authorised mode" by Mastodon, this new feature prevents unauthorised instances and programs from accessing the API. In other words, if instance A blocks instance B, then instance B won't be able to see instance A's public posts anymore. However, this also means mstdn-ebooks can't see your posts. This can be fixed, however, by requesting permission from instance B. So what's the problem?</p>
<p>I would need to ask every single person I run a bot for to click a link I send to them, and then send me back the authentication code they received. This is messy and annoying, and greatly tedious. Furthermore, mstdn-ebooks' code base is starting to show its age (I used to be even worse at programming, believe it or not), and I thought this gave me a good motivation to create a new project.</p>
<p>When FediBooks needs permission from instance B, all you need to do is click "authorise". No copy and pasting codes and sending them in a Patreon DM. This is much cleaner and easier for the end user, which is what this is all about!</p>
<p>FediBooks is easier to use for both me and the end user. I won't have to worry about manually patching ebooks bots, and you won't have to worry about installing and running mstdn-ebooks (or paying me to do it for you). Sounds pretty good, right? 0u0</p>
<p>FediBooks is one of the biggest projects I've ever taken on, and it's been wonderful working on it. I hope you enjoy it!</p>
<h2 class="thin">You used to charge for hosting ebooks bots for people. Why make it free? Aren't you automating yourself out of a job?</h2>
<p>Yup! I'm making this free because I believe all software should be free, not just in cost, but in distributability, accessibility, and so on. mstdn-ebooks is also free software. I used to provide free hosting for mstdn-ebooks bots, but stopped when it became too much for me to handle. In the end, I was running 108 ebooks bots! The worst moment was when I accidentally wiped all the configuration files for them and had to log in to all 108 accounts and re-authorise them all...</p>
<p>Yup! I'm making this free because I believe all software should be free, not just in cost, but in distributability, accessibility, and so on. mstdn-ebooks is also free software, meaning that even though I charged for a hosting service, you could do it yourself for free (or ask someone other than me to do it for you). I used to provide free hosting for mstdn-ebooks bots, but stopped when it became too much for me to handle. In the end, I was running 108 ebooks bots! The worst moment was when I accidentally wiped all the configuration files for them and had to log in to all 108 accounts and re-authorise them all...</p>
<p>FediBooks should (fingers crossed!) require much less maintenance on my part. All I have to do is make sure the server is running, which it is at all times, and make the occasional change or update to reflect new needs. The real concern is my server, not me - let's hope it can handle all these bots!</p>
<p>FediBooks doesn't display ads. It doesn't have any subscription models, donation bonuses, or cryptocurrency mining JavaScript (as useless as that is). It will never have any of these things. What it does have is a donation link at the bottom of the main page. If you have some money to spare and you want to donate, feel free to do so. Paying for the server will be a little tricky since I've just cut off my main source of income, but I should manage. Hopefully.</p>

View file

@ -14,7 +14,7 @@
{%include 'error.html' %}
<div class="container centred">
<form action="/bot/accounts/add" method="POST">
<form method="POST">
{% if session['step'] == 1 %}
<label for="account" class="important">Account handle</label>
<input type="text" name="account" class="full-width" placeholder="@user@example.com">

View file

@ -14,7 +14,7 @@
{% include 'error.html' %}
<div class="container centred">
<form action="/bot/create" method="POST">
<form method="POST">
{% if session['step'] == 1 %}
<label for="instance" class="important">What instance will your bot's account be on?</label>
<input type="text" name="instance" class="full-width" placeholder="botsin.space">

View file

@ -9,57 +9,60 @@
<body>
<div class="container">
<h1 class="thin centred">Configure bot</h1>
<p class="large centred">@botname@example.com</p>
<p class="large centred">{{ bot['handle'] }}</p>
</div>
{% include 'error.html' %}
{% include 'success.html' %}
<div class="container centred">
<form action="/do/bot/edit" method="post" class="full-width">
<div class="row">
<form method="POST" class="full-width">
<!-- <div class="row">
<label for="username" class="large">Username</label>
<input type="text" name="username" value="Bot Name">
</div>
</div> -->
<div class="row">
<label for="freq" class="large">Post frequency (minutes)</label>
<input type="number" min="15" max="240" step="5" value="30" name="freq">
<input type="number" min="15" max="240" step="5" value="{{ bot['post_frequency'] }}" name="freq">
</div>
<div class="row">
<label for="cw" class="large">Content warning (subject)</label>
<input type="text" placeholder="None" name="cw">
<input type="text" placeholder="None" name="cw" pattern=".{0,128}" title="Content warning cannot exceed 128 characters." value = "{{ bot['content_warning'] if bot['content_warning'] != None else '' }}">
</div>
<div class="row">
<label for="length" class="large">Maximum post length (characters)</label>
<input type="number" min="100" max="5000" value="500" name="length">
<input type="number" min="100" max="5000" value="{{ bot['length'] }}" name="length">
</div>
<div class="row">
<label for="fake-mentions" class="large">Fake mentions</label>
<select name="fake-mentions">
<option value="always">At any time</option>
<option value="middle" default>Only in the middle of posts</option>
<option value="never">Never</option>
<option value="always" {{ 'selected' if bot['fake_mentions'] == 'always' }}>At any time</option>
<option value="middle" {{ 'selected' if bot['fake_mentions'] == 'middle' }}>Only in the middle of posts</option>
<option value="never" {{ 'selected' if bot['fake_mentions'] == 'never' }}>Never</option>
</select>
</div>
<div class="row">
<label for="fake-mention-style" class="large">Fake mention style</label>
<select name="fake-mention-style">
<option value="full">@user@instan.ce</option>
<option value="brief" default>@user</option>
<option value="full" {{ 'selected' if bot['fake_mentions_full'] }}>@user@instan.ce</option>
<option value="brief" {{ 'selected' if not bot['fake_mentions_full'] }}>@user</option>
</select>
</div>
<div class="row">
<label for="privacy" class="large">Post privacy</label>
<select name="privacy">
<option value="public">Public</option>
<option value="unlisted">Unlisted</option>
<option value="followers">Followers only</option>
<option value="public" {{ 'selected' if bot['post_privacy'] == 'public' }}>Public</option>
<option value="unlisted" {{ 'selected' if bot['post_privacy'] == 'unlisted' }}>Unlisted</option>
<option value="followers" {{ 'selected' if bot['post_privacy'] == 'private' }}>Followers only</option>
</select>
</div>
<div class="row">
<label for="cw-learning" class="large">Learn from posts with content warnings (subjects)</label>
<input type="checkbox" name="cw-learning">
<input type="checkbox" name="cw-learning" {{ 'checked' if bot['learn_from_cw']}}>
</div>
<div class="row">
<label for="replies" class="large">Enable replies</label>
<input type="checkbox" name="replies">
<input type="checkbox" name="replies" {{ 'checked' if bot['replies_enabled']}}>
</div>
<div class="container centred">
<button class="button btn-primary"><i class="fas fa-save"></i> Save</button>

View file

@ -1,6 +1,6 @@
{% if error != None %}
<div class="error">
<i class="fas fa-exclamation-triangle"></i> {{ error }}
<p class='tiny no-margin'><a class='button btn-small btn-dangerous' href='/issue/bug'><i class="fas fa-exclamation-circle"></i> Report bug</a></p>
<!-- <p class='tiny no-margin'><a class='button btn-small btn-dangerous' href='/issue/bug'><i class="fas fa-exclamation-circle"></i> Report bug</a></p> -->
</div>
{% endif %}

View file

@ -1,5 +1,8 @@
<footer>
<div class='subtle'>
<p>FediBooks is beta software. It might behave unexpectedly. Website design and FediBooks software by <a href='https://fedi.lynnesbian.space/@LynnearSoftware'>Lynne</a>. This site uses <a href="https://fontawesome.com">Font Awesome</a>. Source code available <a href="https://github.com/Lynnesbian/Fedibooks">here</a>.</p>
<div class='subtle small'>
<p>FediBooks is beta software. It might behave unexpectedly. You can learn more about FediBooks <a href="/about">here</a>. <br>
Website design and FediBooks software by <a href='https://fedi.lynnesbian.space/@LynnearSoftware'>Lynne</a>. This site uses <a href="https://fontawesome.com">Font Awesome</a>. <br>
FediBooks uses a cookie to keep you logged in. Deleting this cookie will log you out, and your bots will still work. You can also sign out <a href="/do/signout">here</a>. <br>
Source code is available <a href="https://github.com/Lynnesbian/Fedibooks">here</a> under the AGPLv3 license.</p>
</div>
</footer>

View file

@ -19,7 +19,7 @@
<label for="email" class="important full-width">Email</label>
<input type="email" name="email" placeholder="you@example.com">
{% if signup %}
<p class="small">
<p class="small full-width left-align">
FediBooks requires your email address in order to send you alerts when your bot stops working, and for password resets.
</p>
{% endif %}
@ -27,12 +27,12 @@
<label for="password" class="important full-width">Password</label>
<input type="password" pattern=".{8,}" name="password">
{% if signup %}
<p class="small">
<p class="small full-width left-align">
Passwords must be at least eight characters long.
</p>
{% endif %}
</p>
<input type="submit" value="{% if signup %}Create my account{% else %}Log in{% endif %}" class="button btn-primary full-width">
<input type="submit" value="{% if signup %}Create my account{% else %}Log in{% endif %}" class="button centred btn-primary full-width">
</form>
</div>

View file

@ -13,7 +13,7 @@
{% include 'error.html' %}
{% include 'success.html' %}
<div class="container">
<form method="POST" class="full-width">
<div class="container light">
@ -27,7 +27,7 @@
</div>
<div class="row">
<label for="password" class="large">Password</label>
<input type="password" name="password" placeholder="Unchanged">
<input type="password" name="password"pattern=".{8,}" placeholder="Unchanged">
</div>
<div class="container light">

138
webui.py
View file

@ -30,7 +30,6 @@ def login_check():
@app.route("/")
def home():
if 'user_id' in session:
session['step'] = 1
c = mysql.connection.cursor()
c.execute("SELECT COUNT(*) FROM `bots` WHERE user_id = %s", (session['user_id'],))
bot_count = c.fetchone()[0]
@ -105,10 +104,21 @@ def settings():
pw_hashed = hashlib.sha256(request.form['password'].encode('utf-8')).digest()
pw = bcrypt.hashpw(pw_hashed, bcrypt.gensalt(12))
c.execute("UPDATE users SET password = %s WHERE id = %s", (pw, session['user_id']))
# don't require email verification again if the new email address is the same as the old one
c.execute("SELECT email_verified FROM users WHERE id = %s", (session['user_id'],))
if c.fetchone()[0]:
c.execute("SELECT email FROM users WHERE id = %s", (session['user_id'],))
previous_email = c.fetchone()[0]
email_verified = (previous_email == request.form['email'])
else:
email_verified = False
try:
c.execute("UPDATE users SET email = %s, `fetch` = %s, submit = %s, generation = %s, reply = %s WHERE id = %s", (
c.execute("UPDATE users SET email = %s, email_verified = %s, `fetch` = %s, submit = %s, generation = %s, reply = %s WHERE id = %s", (
request.form['email'],
email_verified,
request.form['fetch-error'],
request.form['submit-error'],
request.form['generation-error'],
@ -124,9 +134,62 @@ def settings():
session['success'] = True
return redirect(url_for('settings'), 303)
@app.route("/bot/edit/<id>")
@app.route("/bot/edit/<id>", methods = ['GET', 'POST'])
def bot_edit(id):
return render_template("coming_soon.html")
if request.method == "GET":
dc = mysql.connection.cursor(MySQLdb.cursors.DictCursor)
dc.execute("SELECT * FROM bots WHERE handle = %s", (id,))
return render_template("bot_edit.html", bot = dc.fetchone(), error = session.pop('error', None), success = session.pop('success', None))
else:
# update stored settings
replies_enabled = 'replies' in request.form
learn_from_cw = 'cw-learning' in request.form
if request.form['fake-mention-style'] not in ['full', 'brief']:
session['error'] = "Invalid setting for fake mention style."
return redirect("/bot/edit/{}".format(id), 303)
if request.form['fake-mentions'] not in ['always', 'middle', 'never']:
session['error'] = "Invalid setting for fake mentions."
return redirect("/bot/edit/{}".format(id), 303)
if request.form['privacy'] not in ['public', 'unlisted', 'private']:
session['error'] = "Invalid setting for post privacy."
return redirect("/bot/edit/{}".format(id), 303)
if int(request.form['length']) < 100 or int(request.form['length']) > 5000:
session['error'] = "Invalid setting for maximum post length."
return redirect("/bot/edit/{}".format(id), 303)
if int(request.form['freq']) < 15 or int(request.form['freq']) > 240 or int(request.form['freq']) % 5:
session['error'] = "Invalid setting for post frequency."
return redirect("/bot/edit/{}".format(id), 303)
if len(request.form['cw']) > 128:
session['error'] = "Content warning cannot exceed 128 characters."
return redirect("/bot/edit/{}".format(id), 303)
c = mysql.connection.cursor()
try:
c.execute("UPDATE bots SET replies_enabled = %s, post_frequency = %s, content_warning = %s, length = %s, fake_mentions = %s, fake_mentions_full = %s, post_privacy = %s, learn_from_cw = %s WHERE handle = %s", (
replies_enabled,
request.form['freq'],
request.form['cw'] if request.form['cw'] != "" else None,
request.form['length'],
request.form['fake-mentions'],
request.form['fake-mention-style'] == 'full',
request.form['privacy'],
learn_from_cw,
id
))
mysql.connection.commit()
c.close()
except:
session['error'] = "Couldn't save your settings."
return redirect("/bot/edit/{}".format(id), 303)
session['success'] = True
return redirect("/bot/edit/{}".format(id), 303)
@app.route("/bot/delete/<id>", methods=['GET', 'POST'])
def bot_delete(id):
@ -198,17 +261,53 @@ def bot_accounts_add():
# 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 '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
uri = re.search(r'template="([^"]+)"', r.text).group(1)
uri = uri.format(uri = "{}@{}".format(username, instance))
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)
j = r.json()
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':
@ -232,8 +331,11 @@ def bot_accounts_add():
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."
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))
@ -281,7 +383,7 @@ def bot_create():
else:
if 'is_pro' in j['contact_account']:
# gab instance
session['error'] = "Eat shit and die, fascist scum."
session['error'] = "Gab instances are not supported."
else:
session['instance_type'] = "Mastodon"
session['step'] += 1
@ -326,11 +428,11 @@ def bot_create():
else:
# the user clicked next on step 2 while having an unsupported instance type
# take them back to step 1
# take them back home
del session['instance']
del session['instance_type']
session['step'] = 1
return redirect(url_for("bot_create"), 303)
return redirect(url_for("home"), 303)
else:
if session['step'] == 4:
@ -357,8 +459,7 @@ def bot_create():
private = privated['privkey']
public = publicd['pubkey']
secret = privated['auth']
# replace fedibooks.com with cfg['base_uri'] on release
client.push_subscription_set("https://fedibooks.com/push/{}".format(handle), publicd, mention_events = True)
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) VALUES (%s, %s, %s, %s, %s, %s)", (handle, session['user_id'], credentials_id, public, private, secret))
mysql.connection.commit()
@ -370,6 +471,10 @@ def bot_create():
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))
@ -447,6 +552,13 @@ def report_bug():
def img_bot_generic():
return send_file("static/bot_generic.png", mimetype="image/png")
@app.route("/favicon.ico")
def favicon():
# so there's a weird bug where one of my firefox installs wants the favicon, and when it can't find it, it requests /
# requesting / resets your session['step'] to 1, which breaks all the multi-step things.
# so we need to give it a favicon.
return send_file("static/favicon.ico")
def bot_check(bot):
# check to ensure bot is owned by user
c = mysql.connection.cursor()

4
wsgi.py Normal file
View file

@ -0,0 +1,4 @@
from webui import app
if __name__ == "__main__":
app.run()