Merge branch 'multi-deployment' into multi-deployment

master
sid 2018-05-28 21:18:22 +02:00 committed by GitHub
commit c9b5fd1d5c
9 changed files with 256 additions and 150 deletions

View File

@ -91,7 +91,7 @@ virtualenv -p python3 .
Install the dependencies: Install the dependencies:
```shell ```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown
``` ```
Configure the bot: Configure the bot:

43
db.py
View File

@ -42,13 +42,13 @@ class DB(object):
CREATE TABLE IF NOT EXISTS triggerpatterns ( CREATE TABLE IF NOT EXISTS triggerpatterns (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER, user_id INTEGER,
pattern TEXT, patterns TEXT,
FOREIGN KEY(user_id) REFERENCES user(id) FOREIGN KEY(user_id) REFERENCES user(id)
); );
CREATE TABLE IF NOT EXISTS badwords ( CREATE TABLE IF NOT EXISTS badwords (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER, user_id INTEGER,
word TEXT, words TEXT,
FOREIGN KEY(user_id) REFERENCES user(id) FOREIGN KEY(user_id) REFERENCES user(id)
); );
CREATE TABLE IF NOT EXISTS mastodon_instances ( CREATE TABLE IF NOT EXISTS mastodon_instances (
@ -137,7 +137,8 @@ class DB(object):
markdown TEXT, markdown 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
); );
''') ''')
@ -149,7 +150,7 @@ class DB(object):
).decode('ascii') ).decode('ascii')
}, self.secret).decode('ascii') }, self.secret).decode('ascii')
def confirm(self, token): def confirm(self, token, city):
from user import User from user import User
try: try:
json = jwt.decode(token, self.secret) json = jwt.decode(token, self.secret)
@ -160,16 +161,37 @@ 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
self.execute(""" default_triggerpatterns = """
INSERT INTO triggerpatterns (user_id, pattern) kontroll?e
VALUES(?, ?); konti
""", (uid, '.*')) db
vgn
vag
zivil
sicherheit
uniform
station
bus
bahn
tram
linie
nuernberg
nürnberg
s\d
u\d\d?
"""
self.execute("""INSERT INTO triggerpatterns (user_id, patterns)
VALUES(?, ?); """, (uid, default_triggerpatterns))
self.execute("INSERT INTO badwords (user_id, words) VALUES(?, ?);",
(uid, "bastard"))
else: else:
uid = json['uid'] uid = json['uid']
self.execute("INSERT INTO email (user_id, email) VALUES(?, ?);", self.execute("INSERT INTO email (user_id, email) VALUES(?, ?);",
(uid, json['email'])) (uid, json['email']))
self.commit() self.commit()
return User(uid) user = User(uid)
user.set_city(city)
return user
def by_email(self, email): def by_email(self, email):
from user import User from user import User
@ -189,7 +211,8 @@ class DB(object):
return dict(city=city, return dict(city=city,
markdown=markdown, markdown=markdown,
masto_link=masto_link, masto_link=masto_link,
twit_link=twit_link) twit_link=twit_link,
mailinglist=city + "@" + config["web"]["host"])
except TypeError: except TypeError:
return None return None

View File

@ -29,6 +29,7 @@ def register_post():
email = request.forms['email'] email = request.forms['email']
password = request.forms['pass'] password = request.forms['pass']
password_repeat = request.forms['pass-repeat'] password_repeat = request.forms['pass-repeat']
city = request.forms['city']
except KeyError: except KeyError:
return dict(error='Please, fill the form.') return dict(error='Please, fill the form.')
if password != password_repeat: if password != password_repeat:
@ -37,11 +38,12 @@ 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
sendmail( sendmail(
email, email,
"Confirm your account", "Confirm your account",
"Complete your registration here: %s" % ( "Complete your registration here: %s" % (
url('confirm/%s' % db.user_token(email, password)) url('confirm/' + city + '/%s' % db.user_token(email, password))
) )
) )
return dict(info='Confirmation mail sent.') return dict(info='Confirmation mail sent.')
@ -50,11 +52,11 @@ def register_post():
return dict(error='Could not send confirmation mail.') return dict(error='Could not send confirmation mail.')
@get('/confirm/<token>') @get('/confirm/<city>/<token>')
@view('template/propaganda.tpl') @view('template/propaganda.tpl')
def confirm(token): def confirm(city, token):
# create db-entry # create db-entry
if db.confirm(token): if db.confirm(token, city):
# :todo show info "Account creation successful." # :todo show info "Account creation successful."
redirect('/settings') redirect('/settings')
return dict(error='Email confirmation failed.') return dict(error='Email confirmation failed.')
@ -76,9 +78,13 @@ def login_post():
@get('/city/<city>') @get('/city/<city>')
@view('template/user-facing.tpl') @view('template/city.tpl')
def city_page(city): def city_page(city):
return db.user_facing_properties(city) 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!')
@get('/settings') @get('/settings')
@ -87,6 +93,27 @@ def settings(user):
return user.state() return user.state()
@post('/settings/markdown')
@view('template/settings.tpl')
def update_markdown(user):
user.set_markdown(request.forms['markdown'])
return user.state()
@post('/settings/goodlist')
@view('template/settings.tpl')
def update_trigger_patterns(user):
user.set_trigger_words(request.forms['goodlist'])
return user.state()
@post('/settings/blacklist')
@view('template/settings.tpl')
def update_badwords(user):
user.set_badwords(request.forms['blacklist'])
return user.state()
@get('/api/state') @get('/api/state')
def api_enable(user): def api_enable(user):
return user.state() return user.state()

BIN
promotion/vag-zeitung.xcf Normal file

Binary file not shown.

9
template/city.tpl Normal file
View File

@ -0,0 +1,9 @@
% rebase('template/wrapper.tpl')
<%
import markdown as md
html = md.markdown(markdown)
%>
{{!html}}

View File

@ -2,6 +2,9 @@
<label for="email">Email</label> <label for="email">Email</label>
<input type="text" placeholder="Enter Email" name="email" id="email" required> <input type="text" placeholder="Enter Email" name="email" id="email" required>
<label for="city">City</label>
<input type='text' name='city' placeholder='Barcelona'/>
<label for="pass">Password</label> <label for="pass">Password</label>
<input type="password" placeholder="Enter Password" name="pass" id="pass" required> <input type="password" placeholder="Enter Password" name="pass" id="pass" required>

View File

@ -1,13 +1,17 @@
% rebase('template/wrapper.tpl') % rebase('template/wrapper.tpl')
<a href="/logout/"><button>Logout</button></a> <a href="/logout/"><button>Logout</button></a>
<div id="enablebutton" style="float: right; padding: 2em;">asdf</div> % if enabled:
<div id="enablebutton" style="float: right; padding: 2em;">Disable</div>
% else:
<div id="enablebutton" style="float: right; padding: 2em;" color="red">Enable</div>
% end
<a class='button' style="padding: 1.5em;" href="/login/twitter"> <a class='button' style="padding: 1.5em;" href="/login/twitter">
<picture> <picture>
<source type='image/webp' sizes='20px' srcset="/static-cb/1517673283/twitter-20.webp 20w,/static-cb/1517673283/twitter-40.webp 40w,/static-cb/1517673283/twitter-80.webp 80w,"/> <source type='image/webp' sizes='20px' srcset="/static-cb/1517673283/twitter-20.webp 20w,/static-cb/1517673283/twitter-40.webp 40w,/static-cb/1517673283/twitter-80.webp 80w,"/>
<source type='image/png' sizes='20px' srcset="/static-cb/1517673283/twitter-20.png 20w,/static-cb/1517673283/twitter-40.png 40w,/static-cb/1517673283/twitter-80.png 80w,"/> <source type='image/png' sizes='20px' srcset="/static-cb/1517673283/twitter-20.png 20w,/static-cb/1517673283/twitter-40.png 40w,/static-cb/1517673283/twitter-80.png 80w,"/>
<img src="https://codl.forget.fr/static-cb/1517673283/twitter-20.png" alt="" /> <img src="https://patriciaannbridewell.files.wordpress.com/2014/04/official-twitter-logo-tile.png" alt="" />
</picture> </picture>
Log in with Twitter Log in with Twitter
</a> </a>
@ -63,10 +67,22 @@
</p> </p>
</section> </section>
<!-- offer mailing list creation button --> <div style="float: left; padding: 1.5em;">
<h2>Edit your city page</h2>
<p>
With your bot, we generated you a page, which you can use for promotion: <a href="/city/{{city}}"
target="_blank">Ticketfrei {{city}}</a> You can change what your users will read there, and adjust it to your
needs. <b>You should definitely adjust the Social Media profile links.</b> This is just the default text we
suggest:
</p>
<form action="/settings/markdown" method="post">
<textarea id="markdown" rows="20" cols="70" name="markdown" wrap="physical">{{markdown}}</textarea>
<input name='confirm' value='Save' type='submit'/>
</form>
</div>
<div style="float: left; padding: 1.5em;"> <div style="float: left; padding: 1.5em;">
<!-- good list entry field --> <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 expressions is in the report, it will be ignored by the bot. If none of these expressions is in the report, it will be ignored by the bot.
@ -74,13 +90,13 @@
</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? -->
<textarea id="goodlist" rows="8" cols="70" name="goodlist" wrap="physical"></textarea> <textarea id="goodlist" rows="8" cols="70" name="goodlist" wrap="physical">{{triggerwords}}</textarea>
<input name='confirm' value='Submit' type='submit'/> <input name='confirm' value='Submit' type='submit'/>
</form> </form>
</div> </div>
<!-- blacklist entry field -->
<div style="float:right; padding: 1.5em;"> <div style="float:right; padding: 1.5em;">
<h2>Edit the blacklist</h2>
<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.
@ -88,7 +104,7 @@
</p> </p>
<form action="/settings/blacklist" method="post"> <form action="/settings/blacklist" method="post">
<!-- find a way to display current blacklist. js which reads from a cookie? template? --> <!-- find a way to display current blacklist. js which reads from a cookie? template? -->
<textarea id="blacklist" rows="8" cols="70" name="blacklist" wrap="physical"></textarea> <textarea id="blacklist" rows="8" cols="70" name="blacklist" wrap="physical">{{badwords}}</textarea>
<input name='confirm' value='Submit' type='submit'/> <input name='confirm' value='Submit' type='submit'/>
</form> </form>
</div> </div>

View File

@ -1,115 +0,0 @@
% rebase('template/wrapper.tpl')
<h1>Wie funktioniert Ticketfrei?</h1>
<p>
Willst du mithelfen, Ticketkontrolleure zu überwachen? Willst du einen
Fahrscheinfreien ÖPNV erkämpfen?
</p>
<h2>Ist es gerade sicher, schwarz zu fahren?</h2>
<p>
Schau dir einfach das Profil unseres Bots an:
<a class="https" href="https://twitter.com/nbg_ticketfrei">https://twitter.com/nbg_ticketfrei</a>
</p>
<p>Hat jemand vor kurzem etwas über Kontrolleur*innen gepostet?</p>
<ul>
<li>
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 also in den letzten
Stunden einen Bericht gab, pass lieber auf.
</li>
<li>
Wenn nicht, ist es wahrscheinlich kein Problem :)
</li>
</ul>
<p>
Also, wenn du weniger Glück hast, und der erste bist, der einen Kontrolleur
sieht:
</p>
<h2>Was mache ich, wenn ich Kontis sehe?</h2>
<p>Ganz einfach, du schreibst es den anderen. Das geht entweder</p>
<ul>
<li>
mit Mastodon
</li>
<li>
über Twitter
</li>
<li>
oder per Mail, falls ihr kein Social Media habt.
</li>
</ul>
<p>
Schreibe einfach einen Toot oder einen Tweet, der den Bot mentioned, und
gib an
</p>
<ul>
<li>
Wo du die Kontis gesehen hast
</li>
<li>
Welche Linie sie benutzen und in welche Richtung sie fahren.
</li>
</ul>
<p>Zum Beispiel so:</p>
<img src="/guides/tooting_screenshot.png" alt="Screenshot of writing a toot" />
<img src="/guides/toot_screenshot.png" alt="A toot ready to be boosted" />
<p>
Der Bot wird die Nachricht dann weiterverbreiten, auch zu den anderen
Netzwerken. Dann können andere Leute das lesen und sicher vor Kontis sein.
</p>
<p>Danke, dass du mithilfst, öffentlichen Verkehr für alle sicherzustellen!</p>
<h2>
Kann ich darauf vertrauen, was random stranger from the Internet mir da
erzählen?
</h2>
<p>Aber natürlich! Wir haben Katzenbilder!</p>
<img src="https://lorempixel.com/550/300/cats" />
<p>
Glaubt besser nicht, wenn jemand postet, dass die Luft da und da gerade
rein ist. Das ist vielleicht sogar gut gemeint - aber klar könnte die VAG
sich hinsetzen und einfach lauter Falschmeldungen posten.
</p>
<p>
Aber Falschmeldungen darüber, dass gerade Kontis i-wo unterwegs sind?
Das macht keinen Sinn. Im schlimmsten Fall kauft jmd mal eine Fahrkarte
mehr - aber kann sonst immer schwarz fahren.</p>
<p>
Also ja - es macht Sinn, uns zu vertrauen, wenn wir sagen, wo gerade Kontis
sind.
</p>
<h2>Was ist Mastodon und warum sollte ich es benutzen?</h2>
<p>
Mastodon ist ein dezentrales soziales Netzwerk - so wie Twitter, nur ohne
Monopol und Zentralismus. Ihr könnt Kurznachrichten (Toots) über alles
mögliche schreiben, und euch mit anderen austauschen.
</p>
<p>
Mastodon ist Open Source, Privatsphäre-freundlich und relativ sicher vor
Zensur.
</p>
<p>
Um Mastodon zu benutzen, könnt ihr euch einen Account zB bei einer dieser
Websiten besorgen:
</p>
<ul>
<li>
<a href="https://queer.party/about">https://queer.party/about</a>
</li>
<li>
<a href="https://soc.ialis.me/about">https://soc.ialis.me/about</a>
</li>
<li>
<a href="https://kitty.town/about">https://kitty.town/about</a>
</li>
<li>
<a class="https" href="https://social.coop/about">https://social.coop/about</a>
</li>
<li>
<a class="https" href="https://awoo.space/about">https://awoo.space/about</a>
</li>
</ul>

165
user.py
View File

@ -28,7 +28,7 @@ class User(object):
@property @property
def enabled(self): def enabled(self):
db.execute("SELECT enabled FROM user WHERE user_id=?;", (self.uid,)) db.execute("SELECT enabled FROM user WHERE id=?;", (self.uid, ))
return bool(db.cur.fetchone()[0]) return bool(db.cur.fetchone()[0])
@enabled.setter @enabled.setter
@ -59,17 +59,35 @@ class User(object):
def is_appropriate(self, report): def is_appropriate(self, report):
db.execute("SELECT pattern FROM triggerpatterns WHERE user_id=?;", db.execute("SELECT pattern FROM triggerpatterns WHERE user_id=?;",
(self.uid,)) (self.uid, ))
for pattern, in db.cur.fetchall(): patterns = db.cur.fetchone()
for pattern in patterns.splitlines():
if pattern.search(report.text) is not None: if pattern.search(report.text) is not None:
break break
else: else:
# no pattern matched # no pattern matched
return False return False
default_badwords = """
bitch
whore
hitler
slut
hure
jude
schwuchtel
fag
faggot
nigger
neger
schlitz
"""
db.execute("SELECT word FROM badwords WHERE user_id=?;", db.execute("SELECT word FROM badwords WHERE user_id=?;",
(self.uid,)) (self.uid, ))
badwords = [word.lower() for word, in db.cur.fetchall()] badwords = db.cur.fetchone()
for word in report.text.lower().split(): for word in report.text.lower().splitlines():
if word in badwords:
return False
for word in default_badwords.splitlines():
if word in badwords: if word in badwords:
return False return False
return True return True
@ -174,13 +192,39 @@ class User(object):
db.execute("UPDATE seen_mail SET mail_date = ? WHERE user_id = ?;", db.execute("UPDATE seen_mail SET mail_date = ? WHERE user_id = ?;",
(mail_date, self.uid)) (mail_date, self.uid))
def get_trigger_words(self, table): def set_trigger_words(self, patterns):
db.execute("""SELECT words db.execute("UPDATE triggerpatterns SET patterns = ? WHERE user_id = ?;",
FROM ? WHERE user_id = ?;""", (table, self.uid,)) (patterns, self.uid))
def get_trigger_words(self):
db.execute("SELECT patterns FROM triggerpatterns WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0]
def set_badwords(self, words):
db.execute("UPDATE badwords SET words = ? WHERE user_id = ?;",
(words, self.uid))
def get_badwords(self):
db.execute("SELECT words FROM badwords WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
def state(self): def state(self):
return dict(foo='bar') # necessary:
# - city
# - markdown
# - goodlist
# - blacklist
# - logged in with twitter?
# - logged in with mastodon?
# - enabled?
citydict = db.user_facing_properties(self.get_city())
return dict(city=citydict['city'],
markdown=citydict['markdown'],
triggerwords=self.get_trigger_words(),
badwords=self.get_badwords(),
enabled=self.enabled)
def save_request_token(self, token): def save_request_token(self, token):
db.execute("""INSERT INTO db.execute("""INSERT INTO
@ -244,6 +288,105 @@ class User(object):
"VALUES(?, ?, ?, ?);", (self.uid, access_token, instance_id, 1)) "VALUES(?, ?, ?, ?);", (self.uid, access_token, instance_id, 1))
db.commit() db.commit()
def set_markdown(self, markdown):
db.execute("UPDATE cities SET markdown = ? WHERE user_id = ?;",
(markdown, self.uid))
def get_city(self): def get_city(self):
db.execute("SELECT city FROM user WHERE id == ?;", (self.uid,)) db.execute("SELECT city FROM cities WHERE user_id == ?;", (self.uid, ))
return db.cur.fetchone()[0] return db.cur.fetchone()[0]
def set_city(self, city):
masto_link = "https://example.mastodon.social/@" + city # get masto_link
twit_link = "https://example.twitter.com/" + city # get twit_link
mailinglist = city + "@" + config['web']['host']
markdown = """# Wie funktioniert Ticketfrei?
Willst du mithelfen, Ticketkontrolleur\*innen zu überwachen?
Willst du einen Fahrscheinfreien ÖPNV erkämpfen?
## Ist es gerade sicher, schwarz zu fahren?
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
also in den letzten Stunden einen Bericht gab, pass lieber
auf.
* Wenn nicht, ist es wahrscheinlich kein Problem :)
Wir können natürlich nicht garantieren, dass es sicher ist,
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.
Also, wenn du weniger Glück hast, und der erste bist, der einen
Kontrolleur sieht:
## Was mache ich, wenn ich Kontis sehe?
Ganz einfach, du schreibst es den anderen. Das geht entweder
* mit Mastodon [Link zu unserem Profil](""" + masto_link + """)
* über Twitter: [Link zu unserem Profil](""" + twit_link + """)
* Oder per Mail an [""" + mailinglist + "](mailto:" + mailinglist + """), wenn
ihr kein Social Media benutzen wollt.
Schreibe einfach einen Toot oder einen Tweet, der den Bot
mentioned, und gib an
* Wo du die Kontis gesehen hast
* Welche Linie sie benutzen und in welche Richtung sie fahren.
Zum Beispiel so:
![Screenshot of writing a Toot](https://github.com/b3yond/ticketfrei/raw/master/guides/tooting_screenshot.png)
![A toot ready to be shared](https://github.com/b3yond/ticketfrei/raw/master/guides/toot_screenshot.png)
Der Bot wird die Nachricht dann weiterverbreiten, auch zu den
anderen Netzwerken.
Dann können andere Leute das lesen und sicher vor Kontis sein.
Danke, dass du mithilfst, öffentlichen Verkehr für alle
sicherzustellen!
## Kann ich darauf vertrauen, was random stranger from the Internet mir da erzählen?
Aber natürlich! Wir haben Katzenbilder!
![Katzenbilder!](https://lorempixel.com/550/300/cats)
Glaubt besser nicht, wenn jemand postet, dass die Luft da und
da gerade rein ist.
Das ist vielleicht sogar gut gemeint - aber klar könnte die
VAG sich hinsetzen und einfach lauter Falschmeldungen posten.
Aber Falschmeldungen darüber, dass gerade Kontis i-wo unterwegs
sind?
Das macht keinen Sinn.
Im schlimmsten Fall kauft jmd mal eine Fahrkarte mehr - aber
kann sonst immer schwarz fahren.
Also ja - es macht Sinn, uns zu vertrauen, wenn wir sagen, wo
gerade Kontis sind.
## Was ist Mastodon und warum sollte ich es benutzen?
Mastodon ist ein dezentrales soziales Netzwerk - so wie
Twitter, nur ohne Monopol und Zentralismus.
Ihr könnt Kurznachrichten (Toots) über alles mögliche
schreiben, und euch mit anderen austauschen.
Mastodon ist Open Source, Privatsphäre-freundlich und relativ
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))
db.commit()