diff --git a/.gitignore b/.gitignore index 0349db9..c20e862 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ seen_toots.pickle seen_toots.pickle.part pip-selfcheck.json config.toml +venv/ bin/ include/ lib/ diff --git a/LICENSE b/LICENSE index 723632a..6b24afd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ Copyright (c) 2017 Thomas L Copyright (c) 2017 b3yond +Copyright (c) 2018 sid Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/README.md b/README.md index 1524707..46f5110 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,11 @@ sudo service nginx restart sudo cp deployment/ticketfrei-web.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl start ticketfrei-web.service + +# create and start the backend systemd service +sudo cp deployment/ticketfrei-backend.service /etc/systemd/system +sudo systemctl daemon-reload +sudo systemctl start ticketfrei-backend.service ``` ### Logs @@ -152,3 +157,51 @@ less /var/log/syslog # for the nginx web server: less /var/log/nginx/example.org_error.log ``` + +### Development Install + +If you want to install it locally to develop on it: + +```shell +sudo apt install python3 virtualenv uwsgi uwsgi-plugin-python3 nginx git +sudo git clone https://github.com/b3yond/ticketfrei +cd ticketfrei +git checkout multi-deployment +``` + +Install the necessary packages, create and activate virtualenv: + +```shell +virtualenv -p python3 . +. bin/activate +``` + +Install the dependencies: + +```shell +pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown +``` + +Configure the bot: + +```shell +cp config.toml.example config.toml +vim config.toml +``` + +This configuration is only for the admin. Users can log into +twitter/mastodon/mail and configure their personal bot on the settings page. + +```shell +# create folder for socket & database +sudo mkdir /var/ticketfrei +sudo chown $USER:$USER -R /var/ticketfrei + +# create folder for logs +sudo mkdir /var/log/ticketfrei +sudo chown $USER:$USER -R /var/log/ticketfrei + +# start Ticketfrei +./frontend.py & ./backend.py & +``` + diff --git a/active_bots/__init__.py b/active_bots/__init__.py new file mode 100644 index 0000000..eedb95f --- /dev/null +++ b/active_bots/__init__.py @@ -0,0 +1,15 @@ +__all__ = [] + +import pkgutil +import inspect + +for loader, name, is_pkg in pkgutil.walk_packages(__path__): + module = loader.find_module(name).load_module(name) + + for name, value in inspect.getmembers(module): + if name.startswith('__'): + continue + + globals()[name] = value + __all__.append(name) + diff --git a/active_bots/mailbot.py b/active_bots/mailbot.py index 995b390..f51fb26 100644 --- a/active_bots/mailbot.py +++ b/active_bots/mailbot.py @@ -1,114 +1,71 @@ #!/usr/bin/env python3 -from config import config import logging import sendmail -import ssl import datetime +import mailbox import email -import imaplib import report from bot import Bot - +from config import config +from db import db logger = logging.getLogger(__name__) class Mailbot(Bot): - """ - Bot which sends Mails if mentioned via twitter/mastodon, and tells - other bots that it received mails. - """ - - def login(self, user): - # change to use mailbox of local system - mailinglist = user.get_mailinglist() - mailbox = imaplib.IMAP4_SSL(mailinglist) - context = ssl.create_default_context() - try: - mailbox.starttls(ssl_context=context) - except: - logger.error('StartTLS failed', exc_info=True) - try: - mailbox.login(config["mail"]["user"], - config["mail"]["passphrase"]) - except imaplib.IMAP4.error: - logger.error("Login to mail server failed", exc_info=True) - try: - mailer = sendmail.Mailer() - mailer.send('', config['web']['contact'], - 'Ticketfrei Crash Report', - attachment=config['logging']['logpath']) - except: - logger.error('Mail sending failed', exc_info=True) + # returns a list of Report objects def crawl(self, user): - """ - crawl for new mails. - :return: msgs: (list of report.Report objects) - """ - mailbox = self.login(user) - try: - rv, data = mailbox.select("Inbox") - except imaplib.IMAP4.abort: - logger.error("Crawling Mail failed", exc_info=True) - rv = False - msgs = [] - if rv == 'OK': - rv, data = mailbox.search(None, "ALL") - if rv != 'OK': - return msgs - - for num in data[0].split(): - rv, data = mailbox.fetch(num, '(RFC822)') - if rv != 'OK': - logger.error("Couldn't fetch mail %s %s" % (rv, str(data))) - return msgs - msg = email.message_from_bytes(data[0][1]) - - # check if email was sent by the bot itself. Different solution? - if not user.get_mailinglist() in msg['From']: - # get a comparable date out of the email - date_tuple = email.utils.parsedate_tz(msg['Date']) - date_tuple = datetime.datetime.fromtimestamp( - email.utils.mktime_tz(date_tuple) - ) - date = int((date_tuple - - datetime.datetime(1970, 1, 1)).total_seconds()) - if date > user.get_seen_mail(): - msgs.append(self.make_report(msg)) - return msgs + reports = [] + mails = mailbox.mbox('/var/mail/test') # todo: adjust to actual mailbox + 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): - """ - sends reports by other sources to a mailing list. + 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) - :param report: (report.Report object) - """ - mailinglist = self.login(user) - if report.source is not self: - mailer = sendmail.Mailer() - mailer.send(report.text, mailinglist, - "Warnung: Kontrolleure gesehen") - def make_report(self, msg): - """ - generates a report out of a mail +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_tuple = email.utils.parsedate_tz(msg['Date']) - date_tuple = datetime.datetime.fromtimestamp( - email.utils.mktime_tz(date_tuple) - ) - date = (date_tuple - datetime.datetime(1970, 1, 1)).total_seconds() + :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.get("From") # get mail author from email header - # :todo take only the part before the @ + 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) - self.last_mail = date - return post + 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() diff --git a/active_bots/mastodonbot.py b/active_bots/mastodonbot.py index 07a2311..a7f735b 100755 --- a/active_bots/mastodonbot.py +++ b/active_bots/mastodonbot.py @@ -18,7 +18,11 @@ class MastodonBot(Bot): :return: list of statuses """ mentions = [] - m = Mastodon(*user.get_masto_credentials()) + try: + m = Mastodon(*user.get_masto_credentials()) + except TypeError: + #logger.error("No Mastodon Credentials in database.", exc_info=True) + return mentions try: notifications = m.notifications() except Exception: @@ -49,7 +53,10 @@ class MastodonBot(Bot): return mentions def post(self, user, report): - m = Mastodon(*user.get_masto_credentials()) + try: + m = Mastodon(*user.get_masto_credentials()) + except TypeError: + return # no mastodon account for this user. if report.source == self: try: m.status_reblog(report.id) diff --git a/active_bots/telegrambot.py b/active_bots/telegrambot.py index f343ea9..a52f197 100644 --- a/active_bots/telegrambot.py +++ b/active_bots/telegrambot.py @@ -14,23 +14,23 @@ class TelegramBot(Bot): reports = [] for update in updates: if update.message.text.lower() == "/start": - user.add_telegram_subscribers(update.message.from.id) - tb.send_message(update.message.from.id, "You are now \ + 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.from.id) - tb.send_message(update.message.from.id, "You are now \ + 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.from.id, "Send reports here to \ + tb.send_message(update.message.sender.id, "Send reports here to \ share them with other users. Use /start and /stop to \ be included/excluded.") #TODO: /help message should be set in frontend else: - reports.append(Report(update.message.from.username,self, - update.message.text,None,update.message.date)) + reports.append(Report(update.message.sender.username, self, + update.message.text, None, update.message.date)) return reports def post(self, user, report): diff --git a/active_bots/twitterDMs.py b/active_bots/twitterDMs.py index deb1a8b..50d2810 100644 --- a/active_bots/twitterDMs.py +++ b/active_bots/twitterDMs.py @@ -18,7 +18,7 @@ class TwitterBot(Bot): consumer_secret=keys[1]) auth.set_access_token(keys[2], # access_token_key keys[3]) # access_token_secret - return tweepy.API(auth) + return tweepy.API(auth, wait_on_rate_limit=True) def crawl(self, user): """ @@ -27,7 +27,10 @@ class TwitterBot(Bot): :return: reports: (list of report.Report objects) """ reports = [] - api = self.get_api(user) + try: + api = self.get_api(user) + except IndexError: + return reports # no twitter account for this user. last_dm = user.get_seen_dm() try: if last_dm == None: diff --git a/active_bots/twitterbot.py b/active_bots/twitterbot.py index fdf6045..37b7029 100755 --- a/active_bots/twitterbot.py +++ b/active_bots/twitterbot.py @@ -12,13 +12,14 @@ logger = logging.getLogger(__name__) class TwitterBot(Bot): + def get_api(self, user): - keys = user.get_api_keys() + keys = user.get_twitter_credentials() auth = tweepy.OAuthHandler(consumer_key=keys[0], consumer_secret=keys[1]) auth.set_access_token(keys[2], # access_token_key keys[3]) # access_token_secret - return tweepy.API(auth) + return tweepy.API(auth, wait_on_rate_limit=True) def crawl(self, user): """ @@ -27,7 +28,11 @@ class TwitterBot(Bot): :return: reports: (list of report.Report objects) """ reports = [] - api = self.get_api(user) + try: + api = self.get_api(user) + except Exception: + #logger.error("Error Authenticating Twitter", exc_info=True) + return reports last_mention = user.get_seen_tweet() try: if last_mention == 0: @@ -56,7 +61,10 @@ class TwitterBot(Bot): return [] def post(self, user, report): - api = self.get_api(user) + try: + api = self.get_api(user) + except IndexError: + return # no twitter account for this user. try: if report.source == self: api.retweet(report.id) diff --git a/backend.py b/backend.py index b66fcfa..77189f3 100755 --- a/backend.py +++ b/backend.py @@ -39,5 +39,5 @@ if __name__ == '__main__': bot2.post(user, status) time.sleep(60) # twitter rate limit >.< except Exception: - logger.error('Shutdown.', exc_info=True) + logger.error("Shutdown.", exc_info=True) shutdown() diff --git a/bot.py b/bot.py index b003ab8..c288140 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,8 @@ class Bot(object): # returns a list of Report objects def crawl(self, user): - pass + reports = [] + return reports # post/boost Report object def post(self, user, report): diff --git a/config.toml.example b/config.toml.example index ec7a14b..dca90a7 100644 --- a/config.toml.example +++ b/config.toml.example @@ -6,6 +6,7 @@ consumer_secret = "your_consumer_secret" [web] host = "0.0.0.0" # will be used by bottle as a host. +port = 80 contact = "b3yond@riseup.net" [mail] diff --git a/db.py b/db.py index 7bcc6f7..a55158e 100644 --- a/db.py +++ b/db.py @@ -14,7 +14,7 @@ class DB(object): self.conn = sqlite3.connect(dbfile) self.cur = self.conn.cursor() self.create() - self.secret = urandom(32) + self.secret = self.get_secret() def execute(self, *args, **kwargs): return self.cur.execute(*args, **kwargs) @@ -90,6 +90,13 @@ class DB(object): active INTEGER, FOREIGN KEY(user_id) REFERENCES user(id) ); + CREATE TABLE IF NOT EXISTS telegram_accounts ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, + user_id INTEGER, + apikey TEXT, + active INTEGER, + FOREIGN KEY(user_id) REFERENCES user(id) + ); CREATE TABLE IF NOT EXISTS seen_tweets ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, user_id INTEGER, @@ -127,7 +134,12 @@ class DB(object): id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, user_id INTEGER, 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) ); CREATE TABLE IF NOT EXISTS cities ( @@ -135,14 +147,45 @@ class DB(object): user_id INTEGER, city TEXT, markdown TEXT, + mail_md TEXT, masto_link TEXT, twit_link TEXT, FOREIGN KEY(user_id) REFERENCES user(id), 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): + """ + This function is called by the register confirmation process. It wants + to write an email to the email table and a passhash to the user table. + + :param email: a string with an E-Mail address. + :param password: a string with a passhash. + :return: + """ return jwt.encode({ 'email': email, 'passhash': scrypt_mcf( @@ -150,6 +193,26 @@ class DB(object): ).decode('ascii') }, self.secret).decode('ascii') + def mail_subscription_token(self, email, city): + """ + This function is called by the mail subscription process. It wants + to write an email to the mailinglist table. + + :param email: string + :param city: string + :return: a token with an encoded json dict { email: x, city: y } + """ + token = jwt.encode({ + 'email': email, + 'city': city + }, self.secret).decode('ascii') + return token + + def confirm_subscription(self, token): + json = jwt.decode(token, self.secret) + return json['email'], json['city'] + + def confirm(self, token, city): from user import User try: @@ -188,8 +251,12 @@ u\d\d? uid = json['uid'] self.execute("INSERT INTO email (user_id, email) VALUES(?, ?);", (uid, json['email'])) + self.execute("""INSERT INTO telegram_accounts (user_id, apikey, + active) VALUES(?, ?, ?);""", (uid, "", 1)) self.commit() user = User(uid) + self.execute("INSERT INTO seen_mail (user_id, mail_date) VALUES (?,?)", + (uid, 0)) user.set_city(city) return user @@ -202,14 +269,24 @@ u\d\d? return None return User(uid) + def by_city(self, city): + from user import User + self.execute("SELECT user_id FROM cities WHERE city=?", (city, )) + try: + uid, = self.cur.fetchone() + except TypeError: + return None + return User(uid) + 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 WHERE city=?;""", (city, )) 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, markdown=markdown, + mail_md=mail_md, masto_link=masto_link, twit_link=twit_link, mailinglist=city + "@" + config["web"]["host"]) diff --git a/deployment/ticketfrei-backend.service b/deployment/ticketfrei-backend.service index db95b88..87dce2e 100644 --- a/deployment/ticketfrei-backend.service +++ b/deployment/ticketfrei-backend.service @@ -9,7 +9,7 @@ ExecStart=/srv/ticketfrei/bin/python3 backend.py #RuntimeDirectory=uwsgi Restart=always KillSignal=SIGQUIT -Type=notify +Type=simple StandardError=syslog NotifyAccess=all diff --git a/frontend.py b/frontend.py index a3ae423..82eebf8 100755 --- a/frontend.py +++ b/frontend.py @@ -9,6 +9,7 @@ from sendmail import sendmail from session import SessionPlugin from mastodon import Mastodon + def url(route): return '%s://%s/%s' % ( request.urlparts.scheme, @@ -78,13 +79,51 @@ def login_post(): @get('/city/') -@view('template/city.tpl') -def city_page(city): +def city_page(city, info=None): citydict = db.user_facing_properties(city) if citydict is not None: - return citydict - redirect('/') - return dict(info='There is no Ticketfrei bot in your city yet. Create one yourself!') + citydict['info'] = info + return bottle.template('template/city.tpl', **citydict) + return bottle.template('template/propaganda.tpl', + **dict(info='There is no Ticketfrei bot in your city' + ' yet. Create one yourself!')) + + +@get('/city/mail/') +@view('template/mail.tpl') +def display_mail_page(city): + user = db.by_city(city) + return user.state() + + +@post('/city/mail/submit/') +def subscribe_mail(city): + email = request.forms['mailaddress'] + token = db.mail_subscription_token(email, city) + confirm_link = url('city/mail/confirm/' + token) + print(confirm_link) # only for local testing + # send mail with code to email + sendmail(email, "Subscribe to Ticketfrei " + city + " Mail Notifications", + 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/') +def confirm_subscribe(token): + email, city = db.confirm_subscription(token) + user = db.by_city(city) + user.add_subscriber(email) + return city_page(city, info="Thanks for subscribing to mail notifications!") + + +@get('/city/mail/unsubscribe/') +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') @@ -100,6 +139,12 @@ def update_markdown(user): 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') @view('template/settings.tpl') def update_trigger_patterns(user): @@ -114,6 +159,14 @@ def update_badwords(user): return user.state() +@post('/settings/telegram') +@view('template/settings.tpl') +def register_telegram(user): + apikey = request.forms['apikey'] + user.set_telegram_key(apikey) + return user.state() + + @get('/api/state') def api_enable(user): return user.state() @@ -211,6 +264,6 @@ bottle.install(SessionPlugin('/')) if __name__ == '__main__': # testing only - bottle.run(host='localhost', port=8080) + bottle.run(host=config["web"]["host"], port=config["web"]["port"]) else: application.catchall = False diff --git a/promotion/vag-zeitung.xcf b/promotion/vag-zeitung.xcf index d86346f..9a63e97 100644 Binary files a/promotion/vag-zeitung.xcf and b/promotion/vag-zeitung.xcf differ diff --git a/promotion/vag-zeitung_p02.xcf b/promotion/vag-zeitung_p02.xcf new file mode 100644 index 0000000..cf2ec66 Binary files /dev/null and b/promotion/vag-zeitung_p02.xcf differ diff --git a/report.py b/report.py index aa34fed..5cf2c13 100644 --- a/report.py +++ b/report.py @@ -19,7 +19,7 @@ class Report(object): :param timestamp: time of the report """ self.author = author - self.type = source + self.source = source self.text = text self.timestamp = timestamp self.id = id diff --git a/static/css/style.css b/static/css/style.css index 54590a4..3dc8126 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -41,3 +41,7 @@ input[type=text], input[type=password] { display: inline-block; border: 1px solid #ccc; } + +h2 { + padding-top: 1em; +} diff --git a/template/city.tpl b/template/city.tpl index 7953c52..5613168 100644 --- a/template/city.tpl +++ b/template/city.tpl @@ -6,4 +6,12 @@ import markdown as md html = md.markdown(markdown) %> +% if info is not None: +
+
+

{{!info}}

+
+
+% end + {{!html}} diff --git a/template/mail.tpl b/template/mail.tpl new file mode 100644 index 0000000..58b58e9 --- /dev/null +++ b/template/mail.tpl @@ -0,0 +1,16 @@ +% rebase('template/wrapper.tpl') + +<% +import markdown as md + +html = md.markdown(mail_md) +%> + +{{!html}} + +
+ + +
+
+

Back to Ticketfrei {{!city}} overview

diff --git a/template/propaganda.tpl b/template/propaganda.tpl index 4a3b034..0aafcbf 100644 --- a/template/propaganda.tpl +++ b/template/propaganda.tpl @@ -1,4 +1,12 @@ % rebase('template/wrapper.tpl') +% if defined('info'): +
+
+

{{!info}}

+
+
+
+% end % include('template/login-plain.tpl')

Features

@@ -42,17 +50,17 @@ reclaim public transportation.

- On short term we want to do this by helping users to avoid - controllers and fines - on long term by pressuring public - transportation companies to offer their services free of - charge, financed by the public. + On short term we want to do this by helping users to avoid + controllers and fines - on long term by pressuring public + transportation companies to offer their services free of + charge, financed by the public.

- Because with Ticketfrei you're able to use trains and - subways for free anyway. Take part and create a new + Because with Ticketfrei you're able to use trains and + subways for free anyway. Take part and create a new understanding of what public transportation should look like!

- - - + + + diff --git a/template/settings.tpl b/template/settings.tpl index cd7e524..acc2b5e 100644 --- a/template/settings.tpl +++ b/template/settings.tpl @@ -16,9 +16,8 @@ Log in with Twitter -
+

Log in with Mastodon

-

@@ -64,16 +63,43 @@
-

-
+<% +# todo: hide this part, if there is already a telegram bot connected. +%> +
+

Connect with Telegram

+

+ If you have a Telegram account, you can register a bot there. Just write to @botfather. You can find detailed + instructions on Bots for + Telegram. +

+

+ The botfather will give you an API key - with the API key, Ticketfrei can use the Telegram bot. Enter it here: +

+
+ + +
+
+ +

Edit your city page

- With your bot, we generated you a page, which you can use for promotion: Ticketfrei {{city}} You can change what your users will read there, and adjust it to your - needs. You should definitely adjust the Social Media profile links. This is just the default text we - suggest: + With your bot, we generated you a page, which you can use for promotion: + Ticketfrei {{city}} You + can change what your users will read there, and adjust it to your + needs. +

+

+ You should definitely adjust the Social Media profile links. + Also consider adding this link to the text: Link to the mail subscription page. Your readers + can use this to subscribe to mail notifications. +

+

+ So this is the default text we suggest:

@@ -81,12 +107,30 @@
-
+
+

Edit your mail subscription page

+

+ There is also a page where users can subscribe to mail notifications: + Ticketfrei {{city}}. + You can change what your users will read there, and adjust it to your + needs. +

+

+ So this is the default text we suggest: +

+
+ + +
+
+ +

Edit your trigger patterns

- These words have to be contained in a report. - If none of these expressions is in the report, it will be ignored by the bot. - You can use the defaults, or enter some expressions specific to your city and language. + These words have to be contained in a report. If none of these + expressions is in the report, it will be ignored by the bot. You can + use the defaults, or enter some expressions specific to your city and + language.

@@ -95,12 +139,12 @@
-
+

Edit the blacklist

- 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. - + 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. + diff --git a/template/wrapper.tpl b/template/wrapper.tpl index 43d8f7e..aa9c93e 100644 --- a/template/wrapper.tpl +++ b/template/wrapper.tpl @@ -1,3 +1,5 @@ + + Ticketfrei - {{get('title', 'A bot against control society!')}} @@ -25,3 +27,4 @@

Contribute on GitHub!

+ diff --git a/user.py b/user.py index b96cbb9..e6b0c8d 100644 --- a/user.py +++ b/user.py @@ -58,11 +58,11 @@ class User(object): }, db.secret).decode('ascii') 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, )) - patterns = db.cur.fetchone() + patterns = db.cur.fetchone()[0] for pattern in patterns.splitlines(): - if pattern.search(report.text) is not None: + if pattern in report.text: break else: # no pattern matched @@ -75,13 +75,14 @@ slut hure jude schwuchtel +schlampe fag faggot nigger neger schlitz """ - db.execute("SELECT word FROM badwords WHERE user_id=?;", + db.execute("SELECT words FROM badwords WHERE user_id=?;", (self.uid, )) badwords = db.cur.fetchone() for word in report.text.lower().splitlines(): @@ -158,6 +159,7 @@ schlitz def save_seen_toot(self, toot_id): db.execute("UPDATE seen_toots SET toot_id = ? WHERE user_id = ?;", (toot_id, self.uid)) + db.commit() def get_seen_tweet(self): db.execute("SELECT tweet_id FROM seen_tweets WHERE user_id = ?;", @@ -167,6 +169,7 @@ schlitz def save_seen_tweet(self, tweet_id): db.execute("UPDATE seen_tweets SET tweet_id = ? WHERE user_id = ?;", (tweet_id, self.uid)) + db.commit() def get_seen_dm(self): db.execute("SELECT message_id FROM seen_dms WHERE user_id = ?;", @@ -176,34 +179,43 @@ schlitz def save_seen_dm(self, tweet_id): db.execute("UPDATE seen_dms SET message_id = ? WHERE user_id = ?;", (tweet_id, self.uid)) + db.commit() def get_mailinglist(self): - db.execute("""SELECT email - FROM mailinglist - WHERE user_id = ? AND active = 1;""", (self.uid,)) - return db.cur.fetchone()[0] + db.execute("SELECT email FROM mailinglist WHERE user_id = ?;", (self.uid, )) + return db.cur.fetchall() 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] def save_seen_mail(self, mail_date): db.execute("UPDATE seen_mail SET mail_date = ? WHERE user_id = ?;", (mail_date, self.uid)) + db.commit() def set_trigger_words(self, patterns): db.execute("UPDATE triggerpatterns SET patterns = ? WHERE user_id = ?;", (patterns, self.uid)) + db.commit() def get_trigger_words(self): db.execute("SELECT patterns FROM triggerpatterns WHERE user_id = ?;", (self.uid,)) return db.cur.fetchone()[0] + def add_subscriber(self, email): + 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() + def set_badwords(self, words): db.execute("UPDATE badwords SET words = ? WHERE user_id = ?;", (words, self.uid)) + db.commit() def get_badwords(self): db.execute("SELECT words FROM badwords WHERE user_id = ?;", @@ -214,6 +226,7 @@ schlitz # necessary: # - city # - markdown + # - mail_md # - goodlist # - blacklist # - logged in with twitter? @@ -222,6 +235,7 @@ schlitz citydict = db.user_facing_properties(self.get_city()) return dict(city=citydict['city'], markdown=citydict['markdown'], + mail_md=citydict['mail_md'], triggerwords=self.get_trigger_words(), badwords=self.get_badwords(), enabled=self.enabled) @@ -254,11 +268,14 @@ schlitz db.commit() def get_twitter_token(self): - db.execute("""SELECT access_token, access_token_secret - FROM twitter_accouts WHERE user_id = ?;""", - (self.uid,)) + db.execute("SELECT client_id, client_secret FROM twitter_accounts WHERE user_id = ?;", + (self.uid, )) return db.cur.fetchall() + def set_telegram_key(self, apikey): + db.execute("UPDATE telegram_accounts SET apikey = ? WHERE user_id = ?;", (apikey, self.uid)) + db.commit() + def get_mastodon_app_keys(self, instance): db.execute("""SELECT client_id, client_secret FROM mastodon_instances @@ -291,6 +308,12 @@ schlitz def set_markdown(self, markdown): db.execute("UPDATE cities SET markdown = ? WHERE user_id = ?;", (markdown, self.uid)) + 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): db.execute("SELECT city FROM cities WHERE user_id == ?;", (self.uid, )) @@ -310,6 +333,7 @@ Willst du einen Fahrscheinfreien ÖPNV erkämpfen? Schau einfach auf das Profil unseres Bots: """ + twit_link + """ Hat jemand vor kurzem etwas über Kontrolleur\*innen gepostet? + * Wenn ja, dann kauf dir vllt lieber ein Ticket. In Nürnberg haben wir die Erfahrung gemacht, dass Kontis normalerweile ungefähr ne Woche aktiv sind, ein paar Stunden am Tag. Wenn es @@ -322,6 +346,11 @@ also pass trotzdem auf, wer auf dem Bahnsteig steht. Aber je mehr Leute mitmachen, desto eher kannst du dir sicher sein, dass wir sie finden, bevor sie uns finden. +Wenn du immer direkt gewarnt werden willst, kannst du auch die +E-Mail-Benachrichtigungen aktivieren. Gib einfach +hier deine +E-Mail-Adresse an. + Also, wenn du weniger Glück hast, und der erste bist, der einen Kontrolleur sieht: @@ -386,7 +415,25 @@ sicher vor Zensur. Um Mastodon zu benutzen, besucht diese Seite: [https://joinmastodon.org/](https://joinmastodon.org/) """ - db.execute("""INSERT INTO cities(user_id, city, markdown, masto_link, - twit_link) VALUES(?,?,?,?,?)""", - (self.uid, city, markdown, masto_link, twit_link)) + mail_md = """# Immer up-to-date + +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() +