mirror of
https://github.com/Lynnesbian/FediBooks/
synced 2024-11-25 16:48:58 +00:00
Compare commits
23 commits
49ddde2b9f
...
2cbad2eff3
Author | SHA1 | Date | |
---|---|---|---|
2cbad2eff3 | |||
28ca5c3d83 | |||
fb1f05d96b | |||
44c9cb0248 | |||
bd5733ef11 | |||
313dbc37fd | |||
b39a10c16f | |||
bc6745ae02 | |||
4de3997b5a | |||
97bfa28694 | |||
787851b13a | |||
6838e16b03 | |||
efa1eef5cc | |||
9af94124b6 | |||
43a1ff62b6 | |||
c9da5d9f40 | |||
d6b43fa6d4 | |||
91c732ca94 | |||
a5706ccd59 | |||
f2d15b7d80 | |||
989a9ec98b | |||
fb017468fd | |||
d7f15601d5 |
17 changed files with 297 additions and 135 deletions
9
config.sample.json
Normal file
9
config.sample.json
Normal 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
|
||||||
|
}
|
|
@ -124,4 +124,5 @@ def make_post(handle):
|
||||||
print(post)
|
print(post)
|
||||||
client.status_post(post, visibility = bot['post_privacy'], spoiler_text = bot['content_warning'])
|
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
100
scrape.py
Normal 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!")
|
92
service.py
92
service.py
|
@ -1,89 +1,11 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from mastodon import Mastodon
|
|
||||||
import MySQLdb
|
import MySQLdb
|
||||||
import requests
|
|
||||||
import markovify
|
|
||||||
from multiprocessing import Pool
|
from multiprocessing import Pool
|
||||||
import json, re
|
import json
|
||||||
import functions
|
import functions
|
||||||
|
|
||||||
cfg = json.load(open('config.json'))
|
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")
|
print("Establishing DB connection")
|
||||||
db = MySQLdb.connect(
|
db = MySQLdb.connect(
|
||||||
host = cfg['db_host'],
|
host = cfg['db_host'],
|
||||||
|
@ -95,19 +17,13 @@ db = MySQLdb.connect(
|
||||||
print("Cleaning up database")
|
print("Cleaning up database")
|
||||||
# delete any fedi accounts we no longer need
|
# delete any fedi accounts we no longer need
|
||||||
cursor = db.cursor()
|
cursor = db.cursor()
|
||||||
cursor.execute("DELETE FROM fedi_accounts WHERE handle NOT IN (SELECT fedi_id FROM bot_learned_accounts);")
|
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)
|
|
||||||
|
|
||||||
print("Generating posts")
|
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()
|
bots = cursor.fetchall()
|
||||||
|
|
||||||
with Pool(8) as p:
|
with Pool(cfg['service_threads']) as p:
|
||||||
p.map(functions.make_post, bots)
|
p.map(functions.make_post, bots)
|
||||||
|
|
||||||
#TODO: other cron tasks should be done here, like updating profile pictures
|
#TODO: other cron tasks should be done here, like updating profile pictures
|
||||||
|
|
|
@ -3,6 +3,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
`email` VARCHAR(128) UNIQUE NOT NULL,
|
`email` VARCHAR(128) UNIQUE NOT NULL,
|
||||||
`password` BINARY(60) NOT NULL,
|
`password` BINARY(60) NOT NULL,
|
||||||
|
`email_verified` BOOLEAN DEFAULT 0,
|
||||||
`fetch` ENUM('always', 'once', 'never') DEFAULT 'once',
|
`fetch` ENUM('always', 'once', 'never') DEFAULT 'once',
|
||||||
`submit` ENUM('always', 'once', 'never') DEFAULT 'once',
|
`submit` ENUM('always', 'once', 'never') DEFAULT 'once',
|
||||||
`generation` 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,
|
`credentials_id` INT NOT NULL,
|
||||||
`push_private_key` BINARY(128) NOT NULL,
|
`push_private_key` BINARY(128) NOT NULL,
|
||||||
`push_public_key` BINARY(128) NOT NULL,
|
`push_public_key` BINARY(128) NOT NULL,
|
||||||
`push_secret` BINARY(16)),
|
`push_secret` BINARY(16),
|
||||||
`enabled` BOOLEAN DEFAULT 0,
|
`enabled` BOOLEAN DEFAULT 0,
|
||||||
`replies_enabled` BOOLEAN DEFAULT 1,
|
`replies_enabled` BOOLEAN DEFAULT 1,
|
||||||
`post_frequency` SMALLINT UNSIGNED DEFAULT 30,
|
`post_frequency` SMALLINT UNSIGNED DEFAULT 30,
|
||||||
|
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 136 KiB |
|
@ -47,6 +47,9 @@ body {
|
||||||
.no-margin {
|
.no-margin {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.left-align {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-icon {
|
.panel-icon {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
@ -135,6 +138,10 @@ a:visited {
|
||||||
color: mediumpurple;
|
color: mediumpurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
margin-top: 10px !important;
|
margin-top: 10px !important;
|
||||||
|
@ -165,8 +172,13 @@ input:focus, select:focus, textarea:focus {
|
||||||
border: 3px mediumpurple solid;
|
border: 3px mediumpurple solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
label, input {
|
label, input {
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
label {
|
label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
|
@ -23,15 +23,16 @@
|
||||||
|
|
||||||
<h2 class="thin">Why create FediBooks?</h2>
|
<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>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>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 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>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 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>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>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>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>
|
<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>
|
<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 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>
|
<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>
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{%include 'error.html' %}
|
{%include 'error.html' %}
|
||||||
|
|
||||||
<div class="container centred">
|
<div class="container centred">
|
||||||
<form action="/bot/accounts/add" method="POST">
|
<form method="POST">
|
||||||
{% if session['step'] == 1 %}
|
{% if session['step'] == 1 %}
|
||||||
<label for="account" class="important">Account handle</label>
|
<label for="account" class="important">Account handle</label>
|
||||||
<input type="text" name="account" class="full-width" placeholder="@user@example.com">
|
<input type="text" name="account" class="full-width" placeholder="@user@example.com">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{% include 'error.html' %}
|
{% include 'error.html' %}
|
||||||
|
|
||||||
<div class="container centred">
|
<div class="container centred">
|
||||||
<form action="/bot/create" method="POST">
|
<form method="POST">
|
||||||
{% if session['step'] == 1 %}
|
{% if session['step'] == 1 %}
|
||||||
<label for="instance" class="important">What instance will your bot's account be on?</label>
|
<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">
|
<input type="text" name="instance" class="full-width" placeholder="botsin.space">
|
||||||
|
|
|
@ -9,57 +9,60 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="thin centred">Configure bot</h1>
|
<h1 class="thin centred">Configure bot</h1>
|
||||||
<p class="large centred">@botname@example.com</p>
|
<p class="large centred">{{ bot['handle'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'error.html' %}
|
||||||
|
{% include 'success.html' %}
|
||||||
|
|
||||||
<div class="container centred">
|
<div class="container centred">
|
||||||
<form action="/do/bot/edit" method="post" class="full-width">
|
<form method="POST" class="full-width">
|
||||||
<div class="row">
|
<!-- <div class="row">
|
||||||
<label for="username" class="large">Username</label>
|
<label for="username" class="large">Username</label>
|
||||||
<input type="text" name="username" value="Bot Name">
|
<input type="text" name="username" value="Bot Name">
|
||||||
</div>
|
</div> -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="freq" class="large">Post frequency (minutes)</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="cw" class="large">Content warning (subject)</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="length" class="large">Maximum post length (characters)</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="fake-mentions" class="large">Fake mentions</label>
|
<label for="fake-mentions" class="large">Fake mentions</label>
|
||||||
<select name="fake-mentions">
|
<select name="fake-mentions">
|
||||||
<option value="always">At any time</option>
|
<option value="always" {{ 'selected' if bot['fake_mentions'] == 'always' }}>At any time</option>
|
||||||
<option value="middle" default>Only in the middle of posts</option>
|
<option value="middle" {{ 'selected' if bot['fake_mentions'] == 'middle' }}>Only in the middle of posts</option>
|
||||||
<option value="never">Never</option>
|
<option value="never" {{ 'selected' if bot['fake_mentions'] == 'never' }}>Never</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="fake-mention-style" class="large">Fake mention style</label>
|
<label for="fake-mention-style" class="large">Fake mention style</label>
|
||||||
<select name="fake-mention-style">
|
<select name="fake-mention-style">
|
||||||
<option value="full">@user@instan.ce</option>
|
<option value="full" {{ 'selected' if bot['fake_mentions_full'] }}>@user@instan.ce</option>
|
||||||
<option value="brief" default>@user</option>
|
<option value="brief" {{ 'selected' if not bot['fake_mentions_full'] }}>@user</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="privacy" class="large">Post privacy</label>
|
<label for="privacy" class="large">Post privacy</label>
|
||||||
<select name="privacy">
|
<select name="privacy">
|
||||||
<option value="public">Public</option>
|
<option value="public" {{ 'selected' if bot['post_privacy'] == 'public' }}>Public</option>
|
||||||
<option value="unlisted">Unlisted</option>
|
<option value="unlisted" {{ 'selected' if bot['post_privacy'] == 'unlisted' }}>Unlisted</option>
|
||||||
<option value="followers">Followers only</option>
|
<option value="followers" {{ 'selected' if bot['post_privacy'] == 'private' }}>Followers only</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="cw-learning" class="large">Learn from posts with content warnings (subjects)</label>
|
<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>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="replies" class="large">Enable replies</label>
|
<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>
|
||||||
<div class="container centred">
|
<div class="container centred">
|
||||||
<button class="button btn-primary"><i class="fas fa-save"></i> Save</button>
|
<button class="button btn-primary"><i class="fas fa-save"></i> Save</button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% if error != None %}
|
{% if error != None %}
|
||||||
<div class="error">
|
<div class="error">
|
||||||
<i class="fas fa-exclamation-triangle"></i> {{ 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<footer>
|
<footer>
|
||||||
<div class='subtle'>
|
<div class='subtle small'>
|
||||||
<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>
|
<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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<label for="email" class="important full-width">Email</label>
|
<label for="email" class="important full-width">Email</label>
|
||||||
<input type="email" name="email" placeholder="you@example.com">
|
<input type="email" name="email" placeholder="you@example.com">
|
||||||
{% if signup %}
|
{% 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.
|
FediBooks requires your email address in order to send you alerts when your bot stops working, and for password resets.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -27,12 +27,12 @@
|
||||||
<label for="password" class="important full-width">Password</label>
|
<label for="password" class="important full-width">Password</label>
|
||||||
<input type="password" pattern=".{8,}" name="password">
|
<input type="password" pattern=".{8,}" name="password">
|
||||||
{% if signup %}
|
{% if signup %}
|
||||||
<p class="small">
|
<p class="small full-width left-align">
|
||||||
Passwords must be at least eight characters long.
|
Passwords must be at least eight characters long.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label for="password" class="large">Password</label>
|
<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>
|
||||||
|
|
||||||
<div class="container light">
|
<div class="container light">
|
||||||
|
|
138
webui.py
138
webui.py
|
@ -30,7 +30,6 @@ def login_check():
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def home():
|
def home():
|
||||||
if 'user_id' in session:
|
if 'user_id' in session:
|
||||||
session['step'] = 1
|
|
||||||
c = mysql.connection.cursor()
|
c = mysql.connection.cursor()
|
||||||
c.execute("SELECT COUNT(*) FROM `bots` WHERE user_id = %s", (session['user_id'],))
|
c.execute("SELECT COUNT(*) FROM `bots` WHERE user_id = %s", (session['user_id'],))
|
||||||
bot_count = c.fetchone()[0]
|
bot_count = c.fetchone()[0]
|
||||||
|
@ -106,9 +105,20 @@ def settings():
|
||||||
pw = bcrypt.hashpw(pw_hashed, bcrypt.gensalt(12))
|
pw = bcrypt.hashpw(pw_hashed, bcrypt.gensalt(12))
|
||||||
c.execute("UPDATE users SET password = %s WHERE id = %s", (pw, session['user_id']))
|
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:
|
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'],
|
request.form['email'],
|
||||||
|
email_verified,
|
||||||
request.form['fetch-error'],
|
request.form['fetch-error'],
|
||||||
request.form['submit-error'],
|
request.form['submit-error'],
|
||||||
request.form['generation-error'],
|
request.form['generation-error'],
|
||||||
|
@ -124,9 +134,62 @@ def settings():
|
||||||
session['success'] = True
|
session['success'] = True
|
||||||
return redirect(url_for('settings'), 303)
|
return redirect(url_for('settings'), 303)
|
||||||
|
|
||||||
@app.route("/bot/edit/<id>")
|
@app.route("/bot/edit/<id>", methods = ['GET', 'POST'])
|
||||||
def bot_edit(id):
|
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'])
|
@app.route("/bot/delete/<id>", methods=['GET', 'POST'])
|
||||||
def bot_delete(id):
|
def bot_delete(id):
|
||||||
|
@ -198,17 +261,53 @@ def bot_accounts_add():
|
||||||
|
|
||||||
# look up user
|
# look up user
|
||||||
handle_list = request.form['account'].split('@')
|
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]
|
username = handle_list[1]
|
||||||
instance = handle_list[2]
|
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
|
# 1. download host-meta to find webfinger URL
|
||||||
r = requests.get("https://{}/.well-known/host-meta".format(instance), timeout=10)
|
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
|
# 2. use webfinger to find user's info page
|
||||||
#TODO: use more reliable method
|
#TODO: use more reliable method
|
||||||
uri = re.search(r'template="([^"]+)"', r.text).group(1)
|
try:
|
||||||
uri = uri.format(uri = "{}@{}".format(username, instance))
|
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)
|
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
|
found = False
|
||||||
for link in j['links']:
|
for link in j['links']:
|
||||||
if link['rel'] == 'self':
|
if link['rel'] == 'self':
|
||||||
|
@ -232,8 +331,11 @@ def bot_accounts_add():
|
||||||
mysql.connection.commit()
|
mysql.connection.commit()
|
||||||
return redirect("/bot/accounts/{}".format(session['bot']), 303)
|
return redirect("/bot/accounts/{}".format(session['bot']), 303)
|
||||||
else:
|
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)
|
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))
|
return render_template("bot_accounts_add.html", error = session.pop('error', None))
|
||||||
|
|
||||||
|
@ -281,7 +383,7 @@ def bot_create():
|
||||||
else:
|
else:
|
||||||
if 'is_pro' in j['contact_account']:
|
if 'is_pro' in j['contact_account']:
|
||||||
# gab instance
|
# gab instance
|
||||||
session['error'] = "Eat shit and die, fascist scum."
|
session['error'] = "Gab instances are not supported."
|
||||||
else:
|
else:
|
||||||
session['instance_type'] = "Mastodon"
|
session['instance_type'] = "Mastodon"
|
||||||
session['step'] += 1
|
session['step'] += 1
|
||||||
|
@ -326,11 +428,11 @@ def bot_create():
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# the user clicked next on step 2 while having an unsupported instance type
|
# 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']
|
||||||
del session['instance_type']
|
del session['instance_type']
|
||||||
session['step'] = 1
|
session['step'] = 1
|
||||||
return redirect(url_for("bot_create"), 303)
|
return redirect(url_for("home"), 303)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if session['step'] == 4:
|
if session['step'] == 4:
|
||||||
|
@ -357,8 +459,7 @@ def bot_create():
|
||||||
private = privated['privkey']
|
private = privated['privkey']
|
||||||
public = publicd['pubkey']
|
public = publicd['pubkey']
|
||||||
secret = privated['auth']
|
secret = privated['auth']
|
||||||
# replace fedibooks.com with cfg['base_uri'] on release
|
client.push_subscription_set("{}/push/{}".format(cfg['base_uri'], handle), publicd, mention_events = True)
|
||||||
client.push_subscription_set("https://fedibooks.com/push/{}".format(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))
|
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()
|
mysql.connection.commit()
|
||||||
|
@ -370,6 +471,10 @@ def bot_create():
|
||||||
del session['instance_type']
|
del session['instance_type']
|
||||||
del session['client_id']
|
del session['client_id']
|
||||||
del session['client_secret']
|
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))
|
return render_template("bot_create.html", error = session.pop('error', None))
|
||||||
|
|
||||||
|
@ -447,6 +552,13 @@ def report_bug():
|
||||||
def img_bot_generic():
|
def img_bot_generic():
|
||||||
return send_file("static/bot_generic.png", mimetype="image/png")
|
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):
|
def bot_check(bot):
|
||||||
# check to ensure bot is owned by user
|
# check to ensure bot is owned by user
|
||||||
c = mysql.connection.cursor()
|
c = mysql.connection.cursor()
|
||||||
|
|
4
wsgi.py
Normal file
4
wsgi.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from webui import app
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
Loading…
Reference in a new issue