Merge remote-tracking branch 'origin/multi-deployment' into multi-deployment

# Conflicts:
#	template/settings.tpl
This commit is contained in:
b3yond 2018-09-15 19:20:25 +02:00
commit f360c4f8fd
21 changed files with 524 additions and 134 deletions

View file

@ -1,5 +1,6 @@
Copyright (c) 2017 Thomas L <tom@dl6tom.de> Copyright (c) 2017 Thomas L <tom@dl6tom.de>
Copyright (c) 2017 b3yond <b3yond@riseup.net> Copyright (c) 2017 b3yond <b3yond@riseup.net>
Copyright (c) 2018 sid
Permission to use, copy, modify, and distribute this software for any Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above purpose with or without fee is hereby granted, provided that the above

View file

@ -50,16 +50,16 @@ to check if something was retweeted in the last hour or something.
To this date, we have never heard of this happening though. To this date, we have never heard of this happening though.
### blacklisting ### blockisting
You also need to edit the goodlist and the blacklist. You can do this on the You also need to edit the goodlist and the blocklist. You can do this on the
website, in the settings of your bot. website, in the settings of your bot.
Just add the words to the goodlist, which you want to require. A report is only Just add the words to the goodlist, which you want to require. A report is only
spread if it contains at least one of them. If you want to RT everything, just spread if it contains at least one of them. If you want to RT everything, just
add a ```*```. add a ```*```.
There is also a blacklist, which you can use to automatically sort out There is also a blocklist, which you can use to automatically sort out
malicious messages. Be careful though, our filter can't read the intention with malicious messages. Be careful though, our filter can't read the intention with
which a word was used. Maybe you wanted it there. which a word was used. Maybe you wanted it there.
@ -91,7 +91,7 @@ virtualenv -p python3 .
Install the dependencies: Install the dependencies:
```shell ```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx
``` ```
Configure the bot: Configure the bot:
@ -179,7 +179,7 @@ virtualenv -p python3 .
Install the dependencies: Install the dependencies:
```shell ```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx
``` ```
Configure the bot: Configure the bot:

View file

@ -12,4 +12,3 @@ for loader, name, is_pkg in pkgutil.walk_packages(__path__):
globals()[name] = value globals()[name] = value
__all__.append(name) __all__.append(name)

72
active_bots/mailbot.py Normal file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3
import logging
import sendmail
import datetime
import mailbox
import email
import report
from bot import Bot
from config import config
from db import db
logger = logging.getLogger(__name__)
class Mailbot(Bot):
# returns a list of Report objects
def crawl(self, user):
reports = []
# todo: adjust to actual mailbox
mails = mailbox.mbox('/var/mail/test')
for msg in mails:
if get_date_from_header(msg['Date']) > user.get_seen_mail():
reports.append(make_report(msg, user))
return reports
# post/boost Report object
def post(self, user, report):
recipients = user.get_mailinglist()
for rec in recipients:
rec = rec[0]
unsubscribe_text = "\n_______\nYou don't want to receive those messages? Unsubscribe with this link: "
body = report.text + unsubscribe_text + config['web']['host'] + "/city/mail/unsubscribe/" \
+ db.mail_subscription_token(rec, user.get_city())
if report.author != rec:
try:
sendmail.sendmail(rec, "Ticketfrei " + user.get_city() +
" Report", body=body)
except Exception:
logger.error("Sending Mail failed.", exc_info=True)
def make_report(msg, user):
"""
generates a report out of a mail
:param msg: email.parser.Message object
:return: post: report.Report object
"""
# get a comparable date out of the email
date = get_date_from_header(msg['Date'])
author = msg['From'] # get mail author from email header
# :todo take only the part in between the < >
text = msg.get_payload()
post = report.Report(author, "mail", text, None, date)
user.save_seen_mail(date)
return post
def get_date_from_header(header):
"""
:param header: msg['Date']
:return: float: total seconds
"""
date_tuple = email.utils.parsedate_tz(header)
date_tuple = datetime.datetime.fromtimestamp(
email.utils.mktime_tz(date_tuple)
)
return (date_tuple - datetime.datetime(1970, 1, 1)).total_seconds()

View file

@ -29,6 +29,8 @@ class MastodonBot(Bot):
logger.error("Unknown Mastodon API Error.", exc_info=True) logger.error("Unknown Mastodon API Error.", exc_info=True)
return mentions return mentions
for status in notifications: for status in notifications:
if user.get_seen_toot() is None:
user.init_seen_toot(m.instance()['uri'])
if (status['type'] == 'mention' and if (status['type'] == 'mention' and
status['status']['id'] > user.get_seen_toot()): status['status']['id'] > user.get_seen_toot()):
# save state # save state
@ -53,7 +55,10 @@ class MastodonBot(Bot):
return mentions return mentions
def post(self, user, report): def post(self, user, report):
try:
m = Mastodon(*user.get_masto_credentials()) m = Mastodon(*user.get_masto_credentials())
except TypeError:
return # no mastodon account for this user.
if report.source == self: if report.source == self:
try: try:
m.status_reblog(report.id) m.status_reblog(report.id)
@ -66,4 +71,5 @@ class MastodonBot(Bot):
try: try:
m.toot(text) m.toot(text)
except Exception: except Exception:
logger.error('Error tooting: ' + user.get_city() + ': ' + report.id, exc_info=True) logger.error('Error tooting: ' + user.get_city() + ': ' +
report.id, exc_info=True)

View file

@ -0,0 +1,59 @@
from bot import Bot
import logging
from report import Report
from twx.botapi import TelegramBot as Telegram
logger = logging.getLogger(__name__)
class TelegramBot(Bot):
def crawl(self, user):
tb = Telegram(user.get_telegram_credentials())
seen_tg = user.get_seen_tg()
try:
updates = tb.get_updates(offset=seen_tg + 1,
allowed_updates="message").wait()
except TypeError:
updates = tb.get_updates().wait()
reports = []
if updates == None:
return reports
for update in updates:
if update == 404:
return reports
user.save_seen_tg(update.update_id)
if update.message.text.lower() == "/start":
user.add_telegram_subscribers(update.message.sender.id)
tb.send_message(
update.message.sender.id,
"You are now subscribed to report notifications.")
# TODO: /start message should be set in frontend
elif update.message.text.lower() == "/stop":
user.remove_telegram_subscribers(update.message.sender.id)
tb.send_message(
update.message.sender.id,
"You are now unsubscribed from report notifications.")
# TODO: /stop message should be set in frontend
elif update.message.text.lower() == "/help":
tb.send_message(
update.message.sender.id,
"Send reports here to share them with other users. Use /start and /stop to get reports or not.")
# TODO: /help message should be set in frontend
else:
reports.append(Report(update.message.sender.username, self,
update.message.text, None,
update.message.date))
return reports
def post(self, user, report):
tb = Telegram(user.get_telegram_credentials())
text = report.text
if len(text) > 4096:
text = text[:4096 - 4] + u' ...'
try:
for subscriber_id in user.get_telegram_subscribers():
tb.send_message(subscriber_id, text).wait()
except Exception:
logger.error('Error telegramming: ' + user.get_city() + ': '
+ str(report.id), exc_info=True)

View file

@ -27,10 +27,13 @@ class TwitterBot(Bot):
:return: reports: (list of report.Report objects) :return: reports: (list of report.Report objects)
""" """
reports = [] reports = []
try:
api = self.get_api(user) api = self.get_api(user)
except IndexError:
return reports # no twitter account for this user.
last_dm = user.get_seen_dm() last_dm = user.get_seen_dm()
try: try:
if last_dm == None: if last_dm is None:
mentions = api.direct_messages() mentions = api.direct_messages()
else: else:
mentions = api.mentions_timeline(since_id=last_dm[0]) mentions = api.mentions_timeline(since_id=last_dm[0])

View file

@ -4,6 +4,7 @@ import logging
import tweepy import tweepy
import re import re
import requests import requests
from time import time
import report import report
from bot import Bot from bot import Bot
@ -12,6 +13,7 @@ logger = logging.getLogger(__name__)
class TwitterBot(Bot): class TwitterBot(Bot):
def get_api(self, user): def get_api(self, user):
keys = user.get_twitter_credentials() keys = user.get_twitter_credentials()
auth = tweepy.OAuthHandler(consumer_key=keys[0], auth = tweepy.OAuthHandler(consumer_key=keys[0],
@ -27,6 +29,11 @@ class TwitterBot(Bot):
:return: reports: (list of report.Report objects) :return: reports: (list of report.Report objects)
""" """
reports = [] reports = []
try:
if user.get_last_twitter_request() + 60 > time():
return reports
except TypeError:
user.set_last_twitter_request(time())
try: try:
api = self.get_api(user) api = self.get_api(user)
except Exception: except Exception:
@ -38,6 +45,7 @@ class TwitterBot(Bot):
mentions = api.mentions_timeline() mentions = api.mentions_timeline()
else: else:
mentions = api.mentions_timeline(since_id=last_mention) mentions = api.mentions_timeline(since_id=last_mention)
user.set_last_twitter_request(time())
for status in mentions: for status in mentions:
text = re.sub( text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)", "(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
@ -60,7 +68,10 @@ class TwitterBot(Bot):
return [] return []
def post(self, user, report): def post(self, user, report):
try:
api = self.get_api(user) api = self.get_api(user)
except IndexError:
return # no twitter account for this user.
try: try:
if report.source == self: if report.source == self:
api.retweet(report.id) api.retweet(report.id)

View file

@ -37,7 +37,6 @@ if __name__ == '__main__':
continue continue
for bot2 in bots: for bot2 in bots:
bot2.post(user, status) bot2.post(user, status)
time.sleep(60) # twitter rate limit >.<
except Exception: except Exception:
logger.error("Shutdown.", exc_info=True) logger.error("Shutdown.", exc_info=True)
shutdown() shutdown()

View file

@ -6,6 +6,7 @@ consumer_secret = "your_consumer_secret"
[web] [web]
host = "0.0.0.0" # will be used by bottle as a host. host = "0.0.0.0" # will be used by bottle as a host.
port = 80
contact = "b3yond@riseup.net" contact = "b3yond@riseup.net"
[mail] [mail]

79
db.py
View file

@ -14,7 +14,7 @@ class DB(object):
self.conn = sqlite3.connect(dbfile) self.conn = sqlite3.connect(dbfile)
self.cur = self.conn.cursor() self.cur = self.conn.cursor()
self.create() self.create()
self.secret = urandom(32) self.secret = self.get_secret()
def execute(self, *args, **kwargs): def execute(self, *args, **kwargs):
return self.cur.execute(*args, **kwargs) return self.cur.execute(*args, **kwargs)
@ -70,11 +70,17 @@ class DB(object):
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER, user_id INTEGER,
mastodon_accounts_id INTEGER, mastodon_accounts_id INTEGER,
toot_id TEXT, toot_id INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id), FOREIGN KEY(user_id) REFERENCES user(id),
FOREIGN KEY(mastodon_accounts_id) FOREIGN KEY(mastodon_accounts_id)
REFERENCES mastodon_accounts(id) REFERENCES mastodon_accounts(id)
); );
CREATE TABLE IF NOT EXISTS seen_telegrams (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
tg_id INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS twitter_request_tokens ( CREATE TABLE IF NOT EXISTS twitter_request_tokens (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER, user_id INTEGER,
@ -90,6 +96,12 @@ class DB(object):
active INTEGER, active INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id) FOREIGN KEY(user_id) REFERENCES user(id)
); );
CREATE TABLE IF NOT EXISTS twitter_last_request (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
date INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS telegram_accounts ( CREATE TABLE IF NOT EXISTS telegram_accounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER, user_id INTEGER,
@ -115,11 +127,30 @@ class DB(object):
FOREIGN KEY(twitter_accounts_id) FOREIGN KEY(twitter_accounts_id)
REFERENCES twitter_accounts(id) REFERENCES twitter_accounts(id)
); );
CREATE TABLE IF NOT EXISTS telegram_accounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
api_token TEXT,
active INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS telegram_subscribers (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
subscriber_id INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id),
UNIQUE(user_id, subscriber_id) ON CONFLICT IGNORE
);
CREATE TABLE IF NOT EXISTS mailinglist ( CREATE TABLE IF NOT EXISTS mailinglist (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER, user_id INTEGER,
email TEXT, email TEXT,
active INTEGER, FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS seen_mail (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
mail_date REAL,
FOREIGN KEY(user_id) REFERENCES user(id) FOREIGN KEY(user_id) REFERENCES user(id)
); );
CREATE TABLE IF NOT EXISTS cities ( CREATE TABLE IF NOT EXISTS cities (
@ -127,13 +158,36 @@ class DB(object):
user_id INTEGER, user_id INTEGER,
city TEXT, city TEXT,
markdown TEXT, markdown TEXT,
mail_md TEXT,
masto_link TEXT, masto_link TEXT,
twit_link TEXT, twit_link TEXT,
FOREIGN KEY(user_id) REFERENCES user(id), FOREIGN KEY(user_id) REFERENCES user(id),
UNIQUE(user_id, city) ON CONFLICT IGNORE UNIQUE(user_id, city) ON CONFLICT IGNORE
); );
CREATE TABLE IF NOT EXISTS secret (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
secret BLOB
);
''') ''')
def get_secret(self):
"""
At __init__(), the db needs a secret. It tries to fetch it from the db,
and if it fails, it generates a new one.
:return:
"""
# select only the newest secret. should be only one row anyway.
self.execute("SELECT secret FROM secret ORDER BY id DESC LIMIT 1")
try:
return self.cur.fetchone()[0]
except TypeError:
new_secret = urandom(32)
self.execute("INSERT INTO secret (secret) VALUES (?);",
(new_secret, ))
self.commit()
return new_secret
def user_token(self, email, password): def user_token(self, email, password):
""" """
This function is called by the register confirmation process. It wants This function is called by the register confirmation process. It wants
@ -159,16 +213,16 @@ class DB(object):
:param city: string :param city: string
:return: a token with an encoded json dict { email: x, city: y } :return: a token with an encoded json dict { email: x, city: y }
""" """
return jwt.encode({ token = jwt.encode({
'email': email, 'email': email,
'city': city 'city': city
}, self.secret).decode('ascii') }, self.secret).decode('ascii')
return token
def confirm_subscription(self, token): def confirm_subscription(self, token):
json = jwt.decode(token, self.secret) json = jwt.decode(token, self.secret)
return json['email'], json['city'] return json['email'], json['city']
def confirm(self, token, city): def confirm(self, token, city):
from user import User from user import User
try: try:
@ -180,8 +234,7 @@ class DB(object):
self.execute("INSERT INTO user (passhash) VALUES(?);", self.execute("INSERT INTO user (passhash) VALUES(?);",
(json['passhash'], )) (json['passhash'], ))
uid = self.cur.lastrowid uid = self.cur.lastrowid
default_triggerpatterns = """ default_triggerpatterns = """kontroll?e
kontroll?e
konti konti
db db
vgn vgn
@ -197,8 +250,7 @@ linie
nuernberg nuernberg
nürnberg nürnberg
s\d s\d
u\d\d? u\d\d?"""
"""
self.execute("""INSERT INTO triggerpatterns (user_id, patterns) self.execute("""INSERT INTO triggerpatterns (user_id, patterns)
VALUES(?, ?); """, (uid, default_triggerpatterns)) VALUES(?, ?); """, (uid, default_triggerpatterns))
self.execute("INSERT INTO badwords (user_id, words) VALUES(?, ?);", self.execute("INSERT INTO badwords (user_id, words) VALUES(?, ?);",
@ -209,6 +261,10 @@ u\d\d?
(uid, json['email'])) (uid, json['email']))
self.execute("""INSERT INTO telegram_accounts (user_id, apikey, self.execute("""INSERT INTO telegram_accounts (user_id, apikey,
active) VALUES(?, ?, ?);""", (uid, "", 1)) active) VALUES(?, ?, ?);""", (uid, "", 1))
self.execute(
"INSERT INTO seen_telegrams (user_id, tg_id) VALUES (?,?);", (uid, 0))
self.execute(
"INSERT INTO seen_mail (user_id, mail_date) VALUES (?,?);", (uid, 0))
self.commit() self.commit()
user = User(uid) user = User(uid)
user.set_city(city) user.set_city(city)
@ -233,13 +289,14 @@ u\d\d?
return User(uid) return User(uid)
def user_facing_properties(self, city): def user_facing_properties(self, city):
self.execute("""SELECT city, markdown, masto_link, twit_link self.execute("""SELECT city, markdown, mail_md, masto_link, twit_link
FROM cities FROM cities
WHERE city=?;""", (city, )) WHERE city=?;""", (city, ))
try: try:
city, markdown, masto_link, twit_link = self.cur.fetchone() city, markdown, mail_md, masto_link, twit_link = self.cur.fetchone()
return dict(city=city, return dict(city=city,
markdown=markdown, markdown=markdown,
mail_md=mail_md,
masto_link=masto_link, masto_link=masto_link,
twit_link=twit_link, twit_link=twit_link,
mailinglist=city + "@" + config["web"]["host"]) mailinglist=city + "@" + config["web"]["host"])

View file

@ -9,6 +9,7 @@ from sendmail import sendmail
from session import SessionPlugin from session import SessionPlugin
from mastodon import Mastodon from mastodon import Mastodon
def url(route): def url(route):
return '%s://%s/%s' % ( return '%s://%s/%s' % (
request.urlparts.scheme, request.urlparts.scheme,
@ -38,13 +39,13 @@ def register_post():
return dict(error='Email address already in use.') return dict(error='Email address already in use.')
# send confirmation mail # send confirmation mail
try: try:
print(url('confirm/' + city + '/%s' % db.user_token(email, password))) # only for local testing link = url('confirm/' + city + '/%s' % db.user_token(email, password))
print(link) # only for local testing
logger.error('confirmation link to ' + email + ": " + link)
sendmail( sendmail(
email, email,
"Confirm your account", "Confirm your account",
"Complete your registration here: %s" % ( "Complete your registration here: %s" % (link)
url('confirm/' + city + '/%s' % db.user_token(email, password))
)
) )
return dict(info='Confirmation mail sent.') return dict(info='Confirmation mail sent.')
except Exception: except Exception:
@ -78,13 +79,14 @@ def login_post():
@get('/city/<city>') @get('/city/<city>')
@view('template/city.tpl') def city_page(city, info=None):
def city_page(city):
citydict = db.user_facing_properties(city) citydict = db.user_facing_properties(city)
if citydict is not None: if citydict is not None:
return citydict citydict['info'] = info
redirect('/') return bottle.template('template/city.tpl', **citydict)
return dict(info='There is no Ticketfrei bot in your city yet. Create one yourself!') return bottle.template('template/propaganda.tpl',
**dict(info='There is no Ticketfrei bot in your city'
' yet. Create one yourself!'))
@get('/city/mail/<city>') @get('/city/mail/<city>')
@ -102,18 +104,26 @@ def subscribe_mail(city):
print(confirm_link) # only for local testing print(confirm_link) # only for local testing
# send mail with code to email # send mail with code to email
sendmail(email, "Subscribe to Ticketfrei " + city + " Mail Notifications", sendmail(email, "Subscribe to Ticketfrei " + city + " Mail Notifications",
body="To subscribe to the mail notifications for Ticketfrei " + city + ", click on this link: " + token) body="To subscribe to the mail notifications for Ticketfrei " +
city + ", click on this link: " + token)
return city_page(city, info="Thanks! You will receive a confirmation mail.")
@get('/city/mail/confirm/<token>') @get('/city/mail/confirm/<token>')
@view('template/city.tpl')
def confirm_subscribe(token): def confirm_subscribe(token):
email, city = db.confirm_subscription(token) email, city = db.confirm_subscription(token)
print(email) # debug
print(city) # debug
user = db.by_city(city) user = db.by_city(city)
user.add_subscriber(email) user.add_subscriber(email)
redirect('/city/' + city) return city_page(city, info="Thanks for subscribing to mail notifications!")
@get('/city/mail/unsubscribe/<token>')
def unsubscribe(token):
email, city = db.confirm_subscription(token)
user = db.by_city(city)
user.remove_subscriber(email)
return city_page(city, info="You successfully unsubscribed " + email +
" from the mail notifications.")
@get('/settings') @get('/settings')
@ -129,6 +139,13 @@ def update_markdown(user):
return user.state() return user.state()
@post('/settings/mail_md')
@view('template/settings.tpl')
def update_mail_md(user):
user.set_mail_md(request.forms['mail_md'])
return user.state()
@post('/settings/goodlist') @post('/settings/goodlist')
@view('template/settings.tpl') @view('template/settings.tpl')
def update_trigger_patterns(user): def update_trigger_patterns(user):
@ -144,11 +161,10 @@ def update_badwords(user):
@post('/settings/telegram') @post('/settings/telegram')
@view('template/settings.tpl')
def register_telegram(user): def register_telegram(user):
apikey = request.forms['apikey'] apikey = request.forms['apikey']
user.set_telegram_key(apikey) user.update_telegram_key(apikey)
return user.state() return city_page(user.get_city(), info="Thanks for registering Telegram!")
@get('/api/state') @get('/api/state')
@ -221,18 +237,19 @@ def login_mastodon(user):
# get app tokens # get app tokens
instance_url = request.forms.get('instance_url') instance_url = request.forms.get('instance_url')
masto_email = request.forms.get('email') masto_email = request.forms.get('email')
print(masto_email)
masto_pass = request.forms.get('pass') masto_pass = request.forms.get('pass')
print(masto_pass)
client_id, client_secret = user.get_mastodon_app_keys(instance_url) client_id, client_secret = user.get_mastodon_app_keys(instance_url)
m = Mastodon(client_id=client_id, client_secret=client_secret, m = Mastodon(client_id=client_id, client_secret=client_secret,
api_base_url=instance_url) api_base_url=instance_url)
try: try:
access_token = m.log_in(masto_email, masto_pass) access_token = m.log_in(masto_email, masto_pass)
user.save_masto_token(access_token, instance_url) user.save_masto_token(access_token, instance_url)
return dict(
info='Thanks for supporting decentralized social networks!' # Trying to set the seen_toot to 0, thereby initializing it.
) # It should work now, but has default values. Not sure if I need them.
user.init_seen_toot(instance_url)
return city_page(user.get_city(), info='Thanks for supporting decentralized social networks!')
except Exception: except Exception:
logger.error('Login to Mastodon failed.', exc_info=True) logger.error('Login to Mastodon failed.', exc_info=True)
return dict(error='Login to Mastodon failed.') return dict(error='Login to Mastodon failed.')
@ -248,6 +265,6 @@ bottle.install(SessionPlugin('/'))
if __name__ == '__main__': if __name__ == '__main__':
# testing only # testing only
bottle.run(host='localhost', port=8080) bottle.run(host=config["web"]["host"], port=config["web"]["port"])
else: else:
application.catchall = False application.catchall = False

View file

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
class Report(object): class Report(object):
""" """
A ticketfrei report object. A ticketfrei report object.
@ -19,7 +20,7 @@ class Report(object):
:param timestamp: time of the report :param timestamp: time of the report
""" """
self.author = author self.author = author
self.type = source self.source = source
self.text = text self.text = text
self.timestamp = timestamp self.timestamp = timestamp
self.id = id self.id = id

View file

@ -34,7 +34,7 @@ class Mailer(object):
try: try:
context = ssl.create_default_context() context = ssl.create_default_context()
self.s.starttls(context=context) self.s.starttls(context=context)
except: except BaseException: # TODO: Amend specific exception
logger.error('StartTLS failed.', exc_info=True) logger.error('StartTLS failed.', exc_info=True)
self.s.login(config["mail"]["user"], config["mail"]["passphrase"]) self.s.login(config["mail"]["user"], config["mail"]["passphrase"])

View file

@ -83,16 +83,16 @@
</form> </form>
</div> </div>
<!-- blacklist entry field --> <!-- blocklist entry field -->
<div style="float:right; padding: 1.5em;"> <div style="float:right; padding: 1.5em;">
<p> <p>
These words are not allowed in reports. These words are not allowed in reports.
If you encounter spam, you can add more here - the bot will ignore reports which use such words. If you encounter spam, you can add more here - the bot will ignore reports which use such words.
<!-- There are words which you can't exclude from the blacklist, e.g. certain racist, sexist, or antisemitic slurs. (to be implemented) --> <!-- There are words which you can't exclude from the blocklist, e.g. certain racist, sexist, or antisemitic slurs. (to be implemented) -->
</p> </p>
<form action="/settings/blacklist" method="post"> <form action="/settings/blocklist" method="post">
<!-- find a way to display current blacklist. js which reads from a cookie? template? --> <!-- find a way to display current blocklist. js which reads from a cookie? template? -->
<textarea id="blacklist" rows="8" cols="70" name="blacklist" wrap="physical"></textarea> <textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical"></textarea>
<input name='confirm' value='Submit' type='submit'/> <input name='confirm' value='Submit' type='submit'/>
</form> </form>
</div> </div>

View file

@ -36,4 +36,4 @@ document.getElementById("enablebutton").innerHTML = enableButton();
document.getElementById("goodlist").innerHTML = listformat(getCookie("goodlist")); document.getElementById("goodlist").innerHTML = listformat(getCookie("goodlist"));
document.getElementById("blacklist").innerHTML = listformat(getCookie("blacklist")); document.getElementById("blocklist").innerHTML = listformat(getCookie("blocklist"));

View file

@ -6,4 +6,12 @@ import markdown as md
html = md.markdown(markdown) html = md.markdown(markdown)
%> %>
% if info is not None:
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>{{!info}}</p>
</div>
</div>
% end
{{!html}} {{!html}}

View file

@ -3,13 +3,14 @@
<% <%
import markdown as md import markdown as md
html = md.markdown(markdown) html = md.markdown(mail_md)
%> %>
{{!html}}
<form action="/city/mail/submit/{{!city}}" method="post"> <form action="/city/mail/submit/{{!city}}" method="post">
<input type="text" name="mailaddress" placeholder="E-Mail address" id="mailaddress"> <input type="text" name="mailaddress" placeholder="E-Mail address" id="mailaddress">
<input name='confirm' value='Subscribe to E-Mail notifications' type='submit'/> <input name='confirm' value='Subscribe to E-Mail notifications' type='submit'/>
</form> </form>
<br>
<p style="text-align: center;"><a href="/city/{{!city}}">Back to Ticketfrei {{!city}} overview</a></p>
{{!html}}

View file

@ -1,4 +1,12 @@
% rebase('template/wrapper.tpl') % rebase('template/wrapper.tpl')
% if defined('info'):
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>{{!info}}</p>
</div>
</div>
<br>
% end
% include('template/login-plain.tpl') % include('template/login-plain.tpl')
<h1>Features</h1> <h1>Features</h1>
<p> <p>

View file

@ -87,10 +87,20 @@
<div> <div>
<h2>Edit your city page</h2> <h2>Edit your city page</h2>
<p> <p>
With your bot, we generated you a page, which you can use for promotion: <a href="/city/{{city}}" With your bot, we generated you a page, which you can use for promotion:
target="_blank">Ticketfrei {{city}}</a> You can change what your users will read there, and adjust it to your <a href="/city/{{city}}" target="_blank">Ticketfrei {{city}}</a> You
needs. <b>You should definitely adjust the Social Media profile links.</b> This is just the default text we can change what your users will read there, and adjust it to your
suggest: needs.
</p>
<p>
<b>You should definitely adjust the Social Media, E-Mail, and Telegram
profile links.</b>
Also consider adding this link to the text: <a href="/city/mail/{{city}}"
target="_blank">Link to the mail subscription page</a>. Your readers
can use this to subscribe to mail notifications.
</p>
<p>
So this is the default text we suggest:
</p> </p>
<form action="/settings/markdown" method="post"> <form action="/settings/markdown" method="post">
<textarea id="markdown" rows="20" cols="70" name="markdown" wrap="physical">{{markdown}}</textarea> <textarea id="markdown" rows="20" cols="70" name="markdown" wrap="physical">{{markdown}}</textarea>
@ -98,12 +108,30 @@
</form> </form>
</div> </div>
<div>
<h2>Edit your mail subscription page</h2>
<p>
There is also a page where users can subscribe to mail notifications:
<a href="/city/mail/{{city}}" target="_blank">Ticketfrei {{city}}</a>.
You can change what your users will read there, and adjust it to your
needs.
</p>
<p>
So this is the default text we suggest:
</p>
<form action="/settings/mail_md" method="post">
<textarea id="mail_md" rows="20" cols="70" name="mail_md" wrap="physical">{{mail_md}}</textarea>
<input name='confirm' value='Save' type='submit'/>
</form>
</div>
<div> <div>
<h2>Edit your trigger patterns</h2> <h2>Edit your trigger patterns</h2>
<p> <p>
These words have to be contained in a report. These words have to be contained in a report. If none of these
If none of these expressions is in the report, it will be ignored by the bot. expressions is in the report, it will be ignored by the bot. You can
You can use the defaults, or enter some expressions specific to your city and language. use the defaults, or enter some expressions specific to your city and
language.
</p> </p>
<form action="/settings/goodlist" method="post"> <form action="/settings/goodlist" method="post">
<!-- find a way to display current good list. js which reads from a cookie? template? --> <!-- find a way to display current good list. js which reads from a cookie? template? -->
@ -115,11 +143,12 @@
<div> <div>
<h2>Edit the blocklist</h2> <h2>Edit the blocklist</h2>
<p> <p>
These words are not allowed in reports. These words are not allowed in reports. If you encounter spam, you can
If you encounter spam, you can add more here - the bot will ignore reports which use such words. add more here - the bot will ignore reports which use such words.
There are words which you can't exclude from the blocklist, e.g. certain racist, sexist, or antisemitic slurs. <!-- There are words which you can't exclude from the blocklist, e.g. certain racist, sexist, or antisemitic slurs.
</p> </p>
<form action="/settings/blocklist" method="post"> <form action="/settings/blocklist" method="post">
<!-- find a way to display current blocklist. js which reads from a cookie? template? -->
<textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical">{{badwords}}</textarea> <textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical">{{badwords}}</textarea>
<input name='confirm' value='Submit' type='submit'/> <input name='confirm' value='Submit' type='submit'/>
</form> </form>

168
user.py
View file

@ -23,6 +23,7 @@ class User(object):
db.execute("UPDATE user SET passhash=? WHERE id=?;", db.execute("UPDATE user SET passhash=? WHERE id=?;",
(passhash, self.uid)) (passhash, self.uid))
db.commit() db.commit()
password = property(None, password) # setter only, can't read back password = property(None, password) # setter only, can't read back
@property @property
@ -57,11 +58,11 @@ class User(object):
}, db.secret).decode('ascii') }, db.secret).decode('ascii')
def is_appropriate(self, report): def is_appropriate(self, report):
db.execute("SELECT pattern FROM triggerpatterns WHERE user_id=?;", db.execute("SELECT patterns FROM triggerpatterns WHERE user_id=?;",
(self.uid, )) (self.uid, ))
patterns = db.cur.fetchone() patterns = db.cur.fetchone()[0]
for pattern in patterns.splitlines(): for pattern in patterns.splitlines():
if pattern.search(report.text) is not None: if pattern in report.text.lower():
break break
else: else:
# no pattern matched # no pattern matched
@ -81,7 +82,7 @@ nigger
neger neger
schlitz schlitz
""" """
db.execute("SELECT word FROM badwords WHERE user_id=?;", db.execute("SELECT words FROM badwords WHERE user_id=?;",
(self.uid, )) (self.uid, ))
badwords = db.cur.fetchone() badwords = db.cur.fetchone()
for word in report.text.lower().splitlines(): for word in report.text.lower().splitlines():
@ -92,11 +93,45 @@ schlitz
return False return False
return True return True
def get_masto_credentials(self): def get_telegram_credentials(self):
db.execute("SELECT access_token, instance_id FROM mastodon_accounts WHERE user_id = ? AND active = 1;", db.execute("""SELECT apikey
FROM telegram_accounts
WHERE user_id = ? AND active = 1;""",
(self.uid,)) (self.uid,))
row = db.cur.fetchone() row = db.cur.fetchone()
db.execute("SELECT instance, client_id, client_secret FROM mastodon_instances WHERE id = ?;", return row[0]
def get_telegram_subscribers(self):
db.execute("""SELECT subscriber_id
FROM telegram_subscribers
WHERE user_id = ?;""",
(self.uid,))
rows = db.cur.fetchall()
return rows
def add_telegram_subscribers(self, subscriber_id):
db.execute("""INSERT INTO telegram_subscribers (
user_id, subscriber_id) VALUES(?, ?);""",
(self.uid, subscriber_id))
db.commit()
def remove_telegram_subscribers(self, subscriber_id):
db.execute("""DELETE
FROM telegram_subscribers
WHERE user_id = ?
AND subscriber_id = ?;""",
(self.uid, subscriber_id))
db.commit()
def get_masto_credentials(self):
db.execute("""SELECT access_token, instance_id
FROM mastodon_accounts
WHERE user_id = ? AND active = 1;""",
(self.uid,))
row = db.cur.fetchone()
db.execute("""SELECT instance, client_id, client_secret
FROM mastodon_instances
WHERE id = ?;""",
(row[1],)) (row[1],))
instance = db.cur.fetchone() instance = db.cur.fetchone()
return instance[1], instance[2], row[0], instance[0] return instance[1], instance[2], row[0], instance[0]
@ -109,10 +144,32 @@ schlitz
keys.append(row[1]) keys.append(row[1])
return keys return keys
def get_last_twitter_request(self):
db.execute("SELECT date FROM twitter_last_request WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0]
def set_last_twitter_request(self, date):
db.execute("UPDATE twitter_last_request SET date = ? WHERE user_id = ?;",
(date, self.uid))
db.commit()
def init_seen_toot(self, instance_url):
db.execute("SELECT id FROM mastodon_instances WHERE instance = ?;",
(instance_url,))
masto_instance = db.cur.fetchone()[0]
db.execute("INSERT INTO seen_toots (user_id, mastodon_accounts_id, toot_id) VALUES (?,?,?);",
(self.uid, masto_instance, 0))
db.conn.commit()
return
def get_seen_toot(self): def get_seen_toot(self):
db.execute("SELECT toot_id FROM seen_toots WHERE user_id = ?;", db.execute("SELECT toot_id FROM seen_toots WHERE user_id = ?;",
(self.uid,)) (self.uid,))
try:
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
except TypeError:
return None
def save_seen_toot(self, toot_id): def save_seen_toot(self, toot_id):
db.execute("UPDATE seen_toots SET toot_id = ? WHERE user_id = ?;", db.execute("UPDATE seen_toots SET toot_id = ? WHERE user_id = ?;",
@ -139,12 +196,22 @@ schlitz
(tweet_id, self.uid)) (tweet_id, self.uid))
db.commit() db.commit()
def get_mailinglist(self): def get_seen_tg(self):
db.execute("SELECT email FROM mailinglist WHERE user_id = ? AND active = 1;", (self.uid, )) db.execute("SELECT tg_id FROM seen_telegrams WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
def save_seen_tg(self, tg_id):
db.execute("UPDATE seen_telegrams SET tg_id = ? WHERE user_id = ?;",
(tg_id, self.uid))
db.commit()
def get_mailinglist(self):
db.execute("SELECT email FROM mailinglist WHERE user_id = ?;", (self.uid, ))
return db.cur.fetchall()
def get_seen_mail(self): def get_seen_mail(self):
db.execute("SELECT mail_date FROM seen_mails WHERE user_id = ?;", (self.uid, )) db.execute("SELECT mail_date FROM seen_mail WHERE user_id = ?;", (self.uid, ))
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
def save_seen_mail(self, mail_date): def save_seen_mail(self, mail_date):
@ -163,7 +230,11 @@ schlitz
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
def add_subscriber(self, email): def add_subscriber(self, email):
db.execute("INSERT INTO mailinglist(user_id, email, active) VALUES(?, ?, ?);", (self.uid, email, 1)) db.execute("INSERT INTO mailinglist(user_id, email) VALUES(?, ?);", (self.uid, email))
db.commit()
def remove_subscriber(self, email):
db.execute("DELETE FROM mailinglist WHERE email = ? AND user_id = ?;", (email, self.uid))
db.commit() db.commit()
def set_badwords(self, words): def set_badwords(self, words):
@ -180,6 +251,7 @@ schlitz
# necessary: # necessary:
# - city # - city
# - markdown # - markdown
# - mail_md
# - goodlist # - goodlist
# - blocklist # - blocklist
# - logged in with twitter? # - logged in with twitter?
@ -188,26 +260,35 @@ schlitz
citydict = db.user_facing_properties(self.get_city()) citydict = db.user_facing_properties(self.get_city())
return dict(city=citydict['city'], return dict(city=citydict['city'],
markdown=citydict['markdown'], markdown=citydict['markdown'],
mail_md=citydict['mail_md'],
triggerwords=self.get_trigger_words(), triggerwords=self.get_trigger_words(),
badwords=self.get_badwords(), badwords=self.get_badwords(),
enabled=self.enabled) enabled=self.enabled)
def save_request_token(self, token): def save_request_token(self, token):
db.execute("INSERT INTO twitter_request_tokens(user_id, request_token, request_token_secret) VALUES(?, ?, ?);", db.execute("""INSERT INTO
(self.uid, token["oauth_token"], token["oauth_token_secret"])) twitter_request_tokens(
user_id, request_token, request_token_secret
) VALUES(?, ?, ?);""",
(self.uid, token["oauth_token"],
token["oauth_token_secret"]))
db.commit() db.commit()
def get_request_token(self): def get_request_token(self):
db.execute("SELECT request_token, request_token_secret FROM twitter_request_tokens WHERE user_id = ?;", (self.uid,)) db.execute("""SELECT request_token, request_token_secret
FROM twitter_request_tokens
WHERE user_id = ?;""", (self.uid,))
request_token = db.cur.fetchone() request_token = db.cur.fetchone()
db.execute("DELETE FROM twitter_request_tokens WHERE user_id = ?;", (self.uid,)) db.execute("""DELETE FROM twitter_request_tokens
WHERE user_id = ?;""", (self.uid,))
db.commit() db.commit()
return {"oauth_token": request_token[0], return {"oauth_token": request_token[0],
"oauth_token_secret": request_token[1]} "oauth_token_secret": request_token[1]}
def save_twitter_token(self, access_token, access_token_secret): def save_twitter_token(self, access_token, access_token_secret):
db.execute( db.execute(""""INSERT INTO twitter_accounts(
"INSERT INTO twitter_accounts(user_id, client_id, client_secret) VALUES(?, ?, ?);", user_id, client_id, client_secret
) VALUES(?, ?, ?);""",
(self.uid, access_token, access_token_secret)) (self.uid, access_token, access_token_secret))
db.commit() db.commit()
@ -216,12 +297,14 @@ schlitz
(self.uid, )) (self.uid, ))
return db.cur.fetchall() return db.cur.fetchall()
def set_telegram_key(self, apikey): def update_telegram_key(self, apikey):
db.execute("UPDATE telegram_accounts SET apikey = ? WHERE user_id = ?;", (apikey, self.uid)) db.execute("UPDATE telegram_accounts SET apikey = ? WHERE user_id = ?;", (apikey, self.uid))
db.commit() db.commit()
def get_mastodon_app_keys(self, instance): def get_mastodon_app_keys(self, instance):
db.execute("SELECT client_id, client_secret FROM mastodon_instances WHERE instance = ?;", (instance, )) db.execute("""SELECT client_id, client_secret
FROM mastodon_instances
WHERE instance = ?;""", (instance,))
try: try:
row = db.cur.fetchone() row = db.cur.fetchone()
client_id = row[0] client_id = row[0]
@ -229,14 +312,19 @@ schlitz
return client_id, client_secret return client_id, client_secret
except TypeError: except TypeError:
app_name = "ticketfrei" + str(db.secret)[0:4] app_name = "ticketfrei" + str(db.secret)[0:4]
client_id, client_secret = Mastodon.create_app(app_name, api_base_url=instance) client_id, client_secret \
db.execute("INSERT INTO mastodon_instances(instance, client_id, client_secret) VALUES(?, ?, ?);", = Mastodon.create_app(app_name, api_base_url=instance)
db.execute("""INSERT INTO mastodon_instances(
instance, client_id, client_secret
) VALUES(?, ?, ?);""",
(instance, client_id, client_secret)) (instance, client_id, client_secret))
db.commit() db.commit()
return client_id, client_secret return client_id, client_secret
def save_masto_token(self, access_token, instance): def save_masto_token(self, access_token, instance):
db.execute("SELECT id FROM mastodon_instances WHERE instance = ?;", (instance, )) db.execute("""SELECT id
FROM mastodon_instances
WHERE instance = ?;""", (instance,))
instance_id = db.cur.fetchone()[0] instance_id = db.cur.fetchone()[0]
db.execute("INSERT INTO mastodon_accounts(user_id, access_token, instance_id, active) " db.execute("INSERT INTO mastodon_accounts(user_id, access_token, instance_id, active) "
"VALUES(?, ?, ?, ?);", (self.uid, access_token, instance_id, 1)) "VALUES(?, ?, ?, ?);", (self.uid, access_token, instance_id, 1))
@ -247,6 +335,11 @@ schlitz
(markdown, self.uid)) (markdown, self.uid))
db.commit() db.commit()
def set_mail_md(self, mail_md):
db.execute("UPDATE cities SET mail_md = ? WHERE user_id = ?;",
(mail_md, self.uid))
db.commit()
def get_city(self): def get_city(self):
db.execute("SELECT city FROM cities WHERE user_id == ?;", (self.uid, )) db.execute("SELECT city FROM cities WHERE user_id == ?;", (self.uid, ))
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
@ -278,6 +371,12 @@ also pass trotzdem auf, wer auf dem Bahnsteig steht.
Aber je mehr Leute mitmachen, desto eher kannst du dir sicher Aber je mehr Leute mitmachen, desto eher kannst du dir sicher
sein, dass wir sie finden, bevor sie uns finden. sein, dass wir sie finden, bevor sie uns finden.
Wenn du immer direkt gewarnt werden willst, kannst du auch die
Benachrichtigungen über E-Mail oder Telegram aktivieren. Gib
einfach <a href="/city/mail/""" + city + """"/">hier</a> deine
E-Mail-Adresse an oder subscribe dem Telegram-Bot [@ticketfrei_""" + city + \
"_bot](https://t.me/ticketfrei_" + city + """_bot)
Also, wenn du weniger Glück hast, und der erste bist, der einen Also, wenn du weniger Glück hast, und der erste bist, der einen
Kontrolleur sieht: Kontrolleur sieht:
@ -287,6 +386,8 @@ Ganz einfach, du schreibst es den anderen. Das geht entweder
* mit Mastodon [Link zu unserem Profil](""" + masto_link + """) * mit Mastodon [Link zu unserem Profil](""" + masto_link + """)
* über Twitter: [Link zu unserem Profil](""" + twit_link + """) * über Twitter: [Link zu unserem Profil](""" + twit_link + """)
* über Telegram an [@ticketfrei_""" + city + "_bot](https://t.me/ticketfrei_" \
+ city + """_bot)
* Oder per Mail an [""" + mailinglist + "](mailto:" + mailinglist + """), wenn * Oder per Mail an [""" + mailinglist + "](mailto:" + mailinglist + """), wenn
ihr kein Social Media benutzen wollt. ihr kein Social Media benutzen wollt.
@ -342,7 +443,24 @@ sicher vor Zensur.
Um Mastodon zu benutzen, besucht diese Seite: Um Mastodon zu benutzen, besucht diese Seite:
[https://joinmastodon.org/](https://joinmastodon.org/) [https://joinmastodon.org/](https://joinmastodon.org/)
""" """
db.execute("""INSERT INTO cities(user_id, city, markdown, masto_link, mail_md = """# Immer up-to-date
twit_link) VALUES(?,?,?,?,?)""",
(self.uid, city, markdown, masto_link, twit_link)) Du bist viel unterwegs und hast keine Lust, jedes Mal auf das Profil des Bots
zu schauen? Kein Problem. Unsere Mail Notifications benachrichtigen dich, wenn
irgendwo Kontis gesehen werden.
Wenn du uns deine E-Mail-Adresse gibst, kriegst du bei jedem Konti-Report eine
Mail. Wenn du eine Mail-App auf dem Handy hast, so wie
[K9Mail](https://k9mail.github.io/), kriegst du sogar eine Push Notification. So
bist du immer Up-to-date über alles, was im Verkehrsnetz passiert.
## Keine Sorge
Wir benutzen deine E-Mail-Adresse selbstverständlich für nichts anderes. Du
kannst die Benachrichtigungen jederzeit deaktivieren, mit jeder Mail wird ein
unsubscribe-link mitgeschickt.
"""
db.execute("""INSERT INTO cities(user_id, city, markdown, mail_md,
masto_link, twit_link) VALUES(?,?,?,?,?,?)""",
(self.uid, city, markdown, mail_md, masto_link, twit_link))
db.commit() db.commit()