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

Compare commits

..

24 commits

Author SHA1 Message Date
1c67016fa6
emphasise that you need to add accounts to learn from 2020-01-20 15:30:01 +10:00
dc7787d296
more stable code with better error handling 2020-01-20 15:21:46 +10:00
fe01416134
add missing db connection 2020-01-20 14:40:59 +10:00
8cab227531
remove debug code 2020-01-20 14:39:42 +10:00
4b6e563f76
fixed yet another silly issue 2020-01-20 14:38:55 +10:00
07670c4a74
handle running out of posts gracefully 2020-01-20 14:37:39 +10:00
f1a4811094
new generic image 2020-01-20 14:35:58 +10:00
570c62a779
remove commented out stuff 2020-01-20 14:08:41 +10:00
39178dff9c
fix incorrect link 2020-01-20 14:08:30 +10:00
d8dc54f802
only keep 50 most recent messages, disable input box while waiting for a reply 2020-01-20 14:06:39 +10:00
e9cdbf7de2
actually display the error message when there's an error 2020-01-20 13:49:16 +10:00
3d0cdbc5e5
chat UI improvements 2020-01-20 13:46:30 +10:00
d48eb9264f
added a note about JS to the footer 2020-01-20 13:23:40 +10:00
5f089f4040
basic chat functionality 2020-01-20 13:18:59 +10:00
60728c0b35
message generation for bot chat 2020-01-20 13:01:56 +10:00
170a666496
fixed a silly 2020-01-20 12:56:24 +10:00
898f2f7aae
seperated text generation and posting into their own functions 2020-01-20 12:53:11 +10:00
841098cc18
less logspam 2020-01-20 12:52:38 +10:00
34622230b0
allow sending messages in bot chat 2020-01-20 12:43:25 +10:00
11197eb7e7
better englische 2020-01-20 12:22:07 +10:00
9830eeda6b
less log spam 2020-01-20 12:18:17 +10:00
6cfa9ef35f
temporary print statement for debugging 2020-01-20 12:15:27 +10:00
fa60a6569d
less error spam 2020-01-20 12:12:53 +10:00
d06f89ed3f
better mysql instructions 2020-01-20 12:02:22 +10:00
13 changed files with 201 additions and 76 deletions

View file

@ -27,8 +27,7 @@ If this doesn't work, try using ``pip`` instead. If it still doesn't work, you m
```
CREATE DATABASE `fedibooks`;
CREATE USER 'myuser' IDENTIFIED BY 'mypassword';
GRANT USAGE ON *.* TO 'myuser'@localhost IDENTIFIED BY 'mypassword';
GRANT ALL privileges ON `fedibooks`.* TO 'myuser'@localhost;
GRANT ALL PRIVILEGES ON `fedibooks`.* TO 'myuser';
FLUSH PRIVILEGES;
exit
```

View file

@ -34,24 +34,18 @@ def extract_post(post):
text = text.rstrip("\n") # remove trailing newline(s)
return text
def make_post(args):
id = None
acct = None
if len(args) > 1:
id = args[1]
acct = args[3]
handle = args[0]
def generate_output(handle):
db = MySQLdb.connect(
host = cfg['db_host'],
user=cfg['db_user'],
passwd=cfg['db_pass'],
db=cfg['db_name']
)
print("Generating post for {}".format(handle))
# print("Generating post for {}".format(handle))
dc = db.cursor(MySQLdb.cursors.DictCursor)
c = db.cursor()
dc.execute("""
SELECT
SELECT
learn_from_cw,
length,
fake_mentions,
@ -64,17 +58,11 @@ def make_post(args):
FROM
bots, credentials
WHERE
bots.handle = %s
bots.handle = %s
AND bots.credentials_id = credentials.id
""", (handle,))
bot = dc.fetchone()
client = Mastodon(
client_id = bot['client_id'],
client_secret = bot['client_secret'],
access_token = bot['secret'],
api_base_url = "https://{}".format(handle.split("@")[2])
)
# by default, only select posts that don't have CWs.
# if learn_from_cw, then also select posts with CWs
@ -92,7 +80,7 @@ def make_post(args):
# 4. join the list into a string separated by newlines
posts = "\n".join(list(sum(c.fetchall(), ())))
if len(posts) == 0:
print("No posts to learn from.")
print("{} - No posts to learn from.".format(handle))
return
if bot['fake_mentions'] == 'never':
@ -124,30 +112,64 @@ def make_post(args):
if bot['fake_mentions_full']:
post = re.sub(r"@(\w+)@([\w.]+)", r"@{}\1@{}\2".format(zws, zws), post)
else:
post = re.sub(r"@(\w+)@([\w.]+)", r"@{}\1".format(zws), post)
post = re.sub(r"@(\w+)@([\w.]+)", r"@{}\1".format(zws), post)
# also format handles without instances, e.g. @user instead of @user@instan.ce
post = re.sub(r"(?<!\S)@(\w+)", r"@{}\1".format(zws), post)
print(post)
visibility = bot['post_privacy'] if len(args) == 1 else args[2]
visibilities = ['public', 'unlisted', 'private']
if visibilities.index(visibility) < visibilities.index(bot['post_privacy']):
# if post_privacy is set to a more restricted level than the visibility of the post we're replying to, use the user's setting
visibility = bot['post_privacy']
if acct is not None:
post = "{} {}".format(acct, post)
return bot, post
# ensure post isn't longer than bot['length']
post = post[:bot['length']]
# send toot!!
try:
client.status_post(post, id, visibility = visibility, spoiler_text = bot['content_warning'])
except MastodonUnauthorizedError:
# user has revoked the token given to the bot
# this needs to be dealt with properly later on, but for now, we'll just disable the bot
c.execute("UPDATE bots SET enabled = FALSE WHERE handle = %s", (handle,))
def make_post(args):
id = None
acct = None
if len(args) > 1:
id = args[1]
acct = args[3]
handle = args[0]
# print("Generating post for {}".format(handle))
bot, post = generate_output(handle)
client = Mastodon(
client_id = bot['client_id'],
client_secret = bot['client_secret'],
access_token = bot['secret'],
api_base_url = "https://{}".format(handle.split("@")[2])
)
db = MySQLdb.connect(
host = cfg['db_host'],
user=cfg['db_user'],
passwd=cfg['db_pass'],
db=cfg['db_name']
)
c = db.cursor()
# print(post)
visibility = bot['post_privacy'] if len(args) == 1 else args[2]
visibilities = ['public', 'unlisted', 'private']
if visibilities.index(visibility) < visibilities.index(bot['post_privacy']):
# if post_privacy is set to a more restricted level than the visibility of the post we're replying to, use the user's setting
visibility = bot['post_privacy']
if acct is not None:
post = "{} {}".format(acct, post)
# ensure post isn't longer than bot['length']
# TODO: ehhhhhhhhh
post = post[:bot['length']]
# send toot!!
try:
client.status_post(post, id, visibility = visibility, spoiler_text = bot['content_warning'])
except MastodonUnauthorizedError:
# user has revoked the token given to the bot
# this needs to be dealt with properly later on, but for now, we'll just disable the bot
c.execute("UPDATE bots SET enabled = FALSE WHERE handle = %s", (handle,))
except:
print("Failed to create post for {}".format(handle))
if id == None:
# this wasn't a reply, it was a regular post, so update the last post date
c.execute("UPDATE bots SET last_post = CURRENT_TIMESTAMP() WHERE handle = %s", (handle,))
db.commit()
c.close()

View file

@ -47,7 +47,7 @@ def bot_accounts_add(mysql, cfg):
else:
session['instance_type'] = "Mastodon"
session['step'] += 1
else:
error = "Unsupported instance type. Misskey support is planned."
return render_template("bot/accounts_add.html", error = error)
@ -81,12 +81,13 @@ def bot_accounts_add(mysql, cfg):
username = client.account_verify_credentials()['username']
if username != session['username']:
error = "Please authenticate as {}.".format(session['username'])
print("Auth error - {} is not {}".format(session['username'], username))
return render_template("bot/accounts_add.html", error = error)
except:
session['step'] = 1
error = "Authentication failed."
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(session['instance']), timeout=10)
if r.status_code != 200:

View file

@ -19,7 +19,7 @@ def scrape_posts(account):
)
handle = account[0]
outbox = account[1]
print("Scraping {}".format(handle))
# print("Scraping {}".format(handle))
c = db.cursor()
last_post = 0
c.execute("SELECT COUNT(*) FROM `posts` WHERE `fedi_id` = %s", (handle,))
@ -80,9 +80,15 @@ def scrape_posts(account):
if not done:
if pleroma:
r = requests.get(j['next'], timeout = 10)
if 'next' in j:
r = requests.get(j['next'], timeout = 10)
else:
done = True
else:
r = requests.get(j['prev'], timeout = 10)
if 'prev' in j:
r = requests.get(j['prev'], timeout = 10)
else:
done = True
if r.status_code == 429:
# we are now being ratelimited, move on to the next user
@ -94,7 +100,7 @@ def scrape_posts(account):
db.commit()
db.commit()
print("Finished scraping {}".format(handle))
# print("Finished scraping {}".format(handle))
print("Establishing DB connection")
db = MySQLdb.connect(

View file

@ -2,33 +2,55 @@
import MySQLdb
from mastodon import Mastodon
from multiprocessing import Pool
import requests
import json
import functions
cfg = json.load(open('config.json'))
def update_icon(bot):
db = MySQLdb.connect(
host = cfg['db_host'],
user=cfg['db_user'],
passwd=cfg['db_pass'],
db=cfg['db_name'],
use_unicode=True,
charset="utf8mb4"
)
try:
db = MySQLdb.connect(
host = cfg['db_host'],
user=cfg['db_user'],
passwd=cfg['db_pass'],
db=cfg['db_name'],
use_unicode=True,
charset="utf8mb4"
)
except:
print("Failed to connect to database.")
return
url = "https://{}".format(bot['handle'].split("@")[2])
try:
r = requests.head(url, timeout=10, allow_redirects = True)
if r.status_code != 200:
raise
except:
print("{} is down.".format(url))
return
print("Updating cached icon for {}".format(bot['handle']))
client = Mastodon(
client_id = bot['client_id'],
client_secret = bot['client_secret'],
access_token = bot['secret'],
api_base_url = "https://{}".format(bot['handle'].split("@")[2])
api_base_url = url
)
avatar = client.account_verify_credentials()['avatar']
c = db.cursor()
try:
avatar = client.account_verify_credentials()['avatar']
except:
c.execute("UPDATE bots SET icon_update_time = CURRENT_TIMESTAMP() WHERE handle = %s", (bot['handle'],))
db.commit()
c.close()
return
c.execute("UPDATE bots SET icon = %s, icon_update_time = CURRENT_TIMESTAMP() WHERE handle = %s", (avatar, bot['handle']))
db.commit()
c.close()
print("Establishing DB connection")
db = MySQLdb.connect(
@ -48,6 +70,7 @@ db.commit()
print("Generating posts")
cursor.execute("SELECT handle FROM bots WHERE enabled = TRUE AND TIMESTAMPDIFF(MINUTE, last_post, CURRENT_TIMESTAMP()) >= post_frequency")
# cursor.execute("SELECT handle FROM bots WHERE enabled = TRUE")
bots = cursor.fetchall()
with Pool(cfg['service_threads']) as p:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

44
app/static/script.js Normal file
View file

@ -0,0 +1,44 @@
var chatlog = [];
function sendMessage() {
let id = window.location.href.split("/").slice(-1)[0]
message = document.getElementById("chatbox-input-box").value
document.getElementById("chatbox-input-box").value = ''
document.getElementById("chatbox-input-box").disabled = true;
chatlog.push(["user", message])
renderChatlog();
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
if (this.status == 200) {
message = this.responseText.replace("\n", "<br>");
} else {
message = "Encountered an error while trying to get a response.";
}
chatlog.push(["bot", message]);
renderChatlog();
document.getElementById("chatbox-input-box").disabled = false;
}
};
xhttp.open("GET", `/bot/chat/${id}/message`, true);
xhttp.send();
return false;
}
function renderChatlog() {
let chatbox = document.getElementById("chatbox");
let out = "";
if (chatlog.length > 50) {
chatlog.shift(); //only keep the 50 most recent messages to avoid slowdown
}
chatlog.forEach(function(item, i) {
if (item[0] == "user") {
out += `<div class="message-container user"><div class="message user">${item[1]}</div></div>`;
} else {
out += `<div class="message-container bot"><div class="bot-icon"></div><div class="message bot">${item[1]}</div></div>`;
}
})
chatbox.innerHTML = out;
chatbox.scrollTop = chatbox.scrollHeight;
}

View file

@ -217,21 +217,29 @@ form .row {
height: 90vh;
background-color: #3d4353;
padding: 10px;
overflow-y: scroll;
}
#chatbox-input, #chatbox-input input{
width: 100%;
}
#chatbox, #chatbox-input {
max-width: 600px;
margin: 0 auto;
}
#chatbox-input {
display: block;
}
.message {
display: inline-block;
padding: 5px;
min-height: 30px;
max-width: 60%;
margin-bottom: 5px;
}
.message-container.user {
text-align: right;
}
.message-container .bot-icon {
background: center / contain url("https://lynnesbian.space/img/bune.png") no-repeat;
height: 30px;
width: 30px;
display: inline-block;

View file

@ -4,6 +4,17 @@
<meta charset="UTF-8">
<title>FediBooks</title>
{% include 'imports.html' %}
<style>
.bot-icon {
background: center / contain url("{{icon}}") no-repeat;
}
</style>
<script>
window.onload = function() {
document.getElementById("chatbox-input-box").focus();
document.getElementById("chatbox-input").onsubmit = sendMessage;
}
</script>
</head>
<body>
@ -11,7 +22,7 @@
<h1 class="thin centred">Chat</h1>
<p class="centred">Talking to {{ bot }}</p>
<p class="centred" style="margin: 20px 0;">
<a class="button btn-primary" href="/bot/accounts/add" role="button"><i class="fas fa-home"></i> Home</a>
<a class="button btn-primary" href="/" role="button"><i class="fas fa-home"></i> Home</a>
</p>
</div>
@ -23,17 +34,11 @@
<div class="container">
<div id="chatbox">
<div class="message-container user">
<div class="message user">Henlo</div>
</div>
<div class="message-container bot">
<div class="bot-icon"></div>
<div class="message bot">Henlo human uwu<br>How are you</div>
</div>
</div>
<form id="chatbox-input">
<input name="message" placeholder="Press enter to send">
<input id="chatbox-input-box" autocomplete="off" required name="message" placeholder="Press enter to send">
</form>
</div>

View file

@ -12,7 +12,7 @@
</div>
{% include 'error.html' %}
<div class="container centred">
<form method="POST">
{% if session['step'] == 1 %}
@ -26,12 +26,12 @@
{% elif session['step'] == 3 %}
<p>You now need to give your bot access to the {{ session['instance'] }} account you have created for it. If you have not yet created an account on {{ session['instance'] }} for your bot to use, please do so now.</p>
<p>Sign in to the {{ session['instance'] }} account you want your bot to use, then click next to begin the authorisation process.</p>
<p>In another tab, sign in to the {{ session['instance'] }} account you want your bot to use. Once that's done, click next to begin the authorisation process.</p>
{% elif session['step'] == 4 %}
<h2 class="thin centred">Congratulations!</h2>
<p>FediBooks has successfully authenticated with your instance, and your bot is ready to be configured. Click finish to return to the bot management screen.</p>
<p>To get your bot working, you need to add at least one account for it to learn from. You can do so by clicking the <i class="fas fa-users"></i> button. To configure settings such as posting frequency and content warnings, click the <i class="fas fa-cog"></i> button.</p>
<p><strong>Important:</strong> To get your bot working, you need to add at least one account for it to learn from. You can do so by clicking the <i class="fas fa-users"></i> button. To configure settings such as posting frequency and content warnings, click the <i class="fas fa-cog"></i> button.</p>
{% else %}
<h2 class="thin centred">Error</h2>

View file

@ -2,6 +2,7 @@
<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>
Some of FediBooks' functionality requires JavaScript to be enabled, although the core functions such as bot configuration do not. <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>

View file

@ -1,4 +1,5 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://kit-free.fontawesome.com/releases/latest/css/free.min.css">
<link rel='stylesheet' type='text/css' href="{{ url_for('static', filename='style.css') }}" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,700&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,700&display=swap">
<script src="{{ url_for('static', filename='script.js') }}"></script>

View file

@ -76,7 +76,7 @@ def render_delete():
# should never happen ;)
session['error'] = "An unknown error occurred."
return redirect(url_for("render_delete"), 303)
if bcrypt.checkpw(pw_hashed, data['password']):
# passwords match, delete the account
session['error'] = "succ ess"
@ -106,7 +106,7 @@ def render_delete():
c.close()
mysql.connection.commit()
# TODO: show a "deletion successful" message or something
return redirect(url_for("do_signout"), 303)
@ -159,8 +159,20 @@ def bot_toggle(id):
@app.route("/bot/chat/<id>")
def bot_chat(id):
return render_template("coming_soon.html")
# return render_template("/bot/chat.html", bot = id)
# return render_template("coming_soon.html")
if bot_check(id):
c = mysql.connection.cursor()
c.execute("SELECT icon FROM `bots` WHERE handle = %s", (id,))
icon = c.fetchone()[0]
if icon is None:
icon = "/img/bot_generic.png"
return render_template("/bot/chat.html", bot = id, icon = icon)
@app.route("/bot/chat/<id>/message")
def bot_chat_message(id):
if bot_check(id):
_, message = functions.generate_output(id)
return message
@app.route("/bot/blacklist/<id>")
def bot_blacklist(id):
@ -258,9 +270,12 @@ def push(id):
'privkey': int(bot[0].rstrip("\0")),
'auth': bot[1]
}
push_object = client.push_subscription_decrypt_push(request.data, params, request.headers['Encryption'], request.headers['Crypto-Key'])
notification = client.notifications(id = push_object['notification_id'])
me = client.account_verify_credentials()['id']
try:
push_object = client.push_subscription_decrypt_push(request.data, params, request.headers['Encryption'], request.headers['Crypto-Key'])
notification = client.notifications(id = push_object['notification_id'])
me = client.account_verify_credentials()['id']
except:
return "Push failed - do we still have access to {}?".format(id)
# first, check how many times the bot has posted in this thread.
# if it's over 15, don't reply.
@ -329,7 +344,7 @@ def do_login():
if data == None:
session['error'] = "Incorrect login information."
return redirect(url_for("show_login_page"), 303)
if bcrypt.checkpw(pw_hashed, data['password']):
session['user_id'] = data['id']
return redirect(url_for("render_home"))