diff --git a/README.md b/README.md index 7b4603d..2bdc8d7 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ virtualenv -p python3 . Install the dependencies: ```shell -pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt +pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown ``` Configure the bot: diff --git a/db.py b/db.py index e72dac5..7bcc6f7 100644 --- a/db.py +++ b/db.py @@ -42,13 +42,13 @@ class DB(object): CREATE TABLE IF NOT EXISTS triggerpatterns ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, user_id INTEGER, - pattern TEXT, + patterns TEXT, FOREIGN KEY(user_id) REFERENCES user(id) ); CREATE TABLE IF NOT EXISTS badwords ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE, user_id INTEGER, - word TEXT, + words TEXT, FOREIGN KEY(user_id) REFERENCES user(id) ); CREATE TABLE IF NOT EXISTS mastodon_instances ( @@ -137,7 +137,8 @@ class DB(object): markdown TEXT, masto_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') }, self.secret).decode('ascii') - def confirm(self, token): + def confirm(self, token, city): from user import User try: json = jwt.decode(token, self.secret) @@ -160,16 +161,37 @@ class DB(object): self.execute("INSERT INTO user (passhash) VALUES(?);", (json['passhash'], )) uid = self.cur.lastrowid - self.execute(""" - INSERT INTO triggerpatterns (user_id, pattern) - VALUES(?, ?); - """, (uid, '.*')) + default_triggerpatterns = """ +kontroll?e +konti +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: uid = json['uid'] self.execute("INSERT INTO email (user_id, email) VALUES(?, ?);", (uid, json['email'])) self.commit() - return User(uid) + user = User(uid) + user.set_city(city) + return user def by_email(self, email): from user import User @@ -189,7 +211,8 @@ class DB(object): return dict(city=city, markdown=markdown, masto_link=masto_link, - twit_link=twit_link) + twit_link=twit_link, + mailinglist=city + "@" + config["web"]["host"]) except TypeError: return None diff --git a/frontend.py b/frontend.py index 5975b2a..a3ae423 100755 --- a/frontend.py +++ b/frontend.py @@ -29,6 +29,7 @@ def register_post(): email = request.forms['email'] password = request.forms['pass'] password_repeat = request.forms['pass-repeat'] + city = request.forms['city'] except KeyError: return dict(error='Please, fill the form.') if password != password_repeat: @@ -37,11 +38,12 @@ def register_post(): return dict(error='Email address already in use.') # send confirmation mail try: + print(url('confirm/' + city + '/%s' % db.user_token(email, password))) # only for local testing sendmail( email, "Confirm your account", "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.') @@ -50,11 +52,11 @@ def register_post(): return dict(error='Could not send confirmation mail.') -@get('/confirm/') +@get('/confirm//') @view('template/propaganda.tpl') -def confirm(token): +def confirm(city, token): # create db-entry - if db.confirm(token): + if db.confirm(token, city): # :todo show info "Account creation successful." redirect('/settings') return dict(error='Email confirmation failed.') @@ -76,9 +78,13 @@ def login_post(): @get('/city/') -@view('template/user-facing.tpl') +@view('template/city.tpl') 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') @@ -87,6 +93,27 @@ def settings(user): 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') def api_enable(user): return user.state() diff --git a/promotion/vag-zeitung.xcf b/promotion/vag-zeitung.xcf new file mode 100644 index 0000000..d86346f Binary files /dev/null and b/promotion/vag-zeitung.xcf differ diff --git a/template/city.tpl b/template/city.tpl new file mode 100644 index 0000000..7953c52 --- /dev/null +++ b/template/city.tpl @@ -0,0 +1,9 @@ +% rebase('template/wrapper.tpl') + +<% +import markdown as md + +html = md.markdown(markdown) +%> + +{{!html}} diff --git a/template/register-plain.tpl b/template/register-plain.tpl index df26372..57cc637 100644 --- a/template/register-plain.tpl +++ b/template/register-plain.tpl @@ -2,6 +2,9 @@ + + + diff --git a/template/settings.tpl b/template/settings.tpl index 1d34116..cd7e524 100644 --- a/template/settings.tpl +++ b/template/settings.tpl @@ -1,13 +1,17 @@ % rebase('template/wrapper.tpl') -
asdf
+% if enabled: +
Disable
+% else: +
Enable
+% end - + Log in with Twitter @@ -63,10 +67,22 @@

- +
+

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: +

+
+ + +
+
- +

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. @@ -74,13 +90,13 @@

- +
-
+

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. @@ -88,7 +104,7 @@

- +
diff --git a/template/user-facing.tpl b/template/user-facing.tpl deleted file mode 100644 index ff39204..0000000 --- a/template/user-facing.tpl +++ /dev/null @@ -1,115 +0,0 @@ -% rebase('template/wrapper.tpl') - -

Wie funktioniert Ticketfrei?

-

- Willst du mithelfen, Ticketkontrolleure zu überwachen? Willst du einen - Fahrscheinfreien ÖPNV erkämpfen? -

-

Ist es gerade sicher, schwarz zu fahren?

-

- Schau dir einfach das Profil unseres Bots an: - https://twitter.com/nbg_ticketfrei -

-

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 :) -
  • -
-

- 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 -
  • -
  • - über Twitter -
  • -
  • - oder per Mail, falls ihr kein Social Media habt. -
  • -
-

- 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 -A toot ready to be boosted - -

- 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!

- -

- 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, könnt ihr euch einen Account zB bei einer dieser - Websiten besorgen: -

- diff --git a/user.py b/user.py index b28a370..b96cbb9 100644 --- a/user.py +++ b/user.py @@ -28,7 +28,7 @@ class User(object): @property 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]) @enabled.setter @@ -59,17 +59,35 @@ class User(object): def is_appropriate(self, report): db.execute("SELECT pattern FROM triggerpatterns WHERE user_id=?;", - (self.uid,)) - for pattern, in db.cur.fetchall(): + (self.uid, )) + patterns = db.cur.fetchone() + for pattern in patterns.splitlines(): if pattern.search(report.text) is not None: break else: # no pattern matched 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=?;", - (self.uid,)) - badwords = [word.lower() for word, in db.cur.fetchall()] - for word in report.text.lower().split(): + (self.uid, )) + badwords = db.cur.fetchone() + for word in report.text.lower().splitlines(): + if word in badwords: + return False + for word in default_badwords.splitlines(): if word in badwords: return False return True @@ -174,13 +192,39 @@ class User(object): db.execute("UPDATE seen_mail SET mail_date = ? WHERE user_id = ?;", (mail_date, self.uid)) - def get_trigger_words(self, table): - db.execute("""SELECT words - FROM ? WHERE user_id = ?;""", (table, self.uid,)) + def set_trigger_words(self, patterns): + db.execute("UPDATE triggerpatterns SET patterns = ? WHERE user_id = ?;", + (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] 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): db.execute("""INSERT INTO @@ -244,6 +288,105 @@ class User(object): "VALUES(?, ?, ?, ?);", (self.uid, access_token, instance_id, 1)) db.commit() + def set_markdown(self, markdown): + db.execute("UPDATE cities SET markdown = ? WHERE user_id = ?;", + (markdown, self.uid)) + 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] + + 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()