Compare commits

..

86 commits

Author SHA1 Message Date
b3yond 8c778927ee fixing import error 2019-05-17 20:42:13 +02:00
b3yond 079166e74c
Merge pull request #86 from ticketfrei/masto502
don't log Mastodon 502 errors.
2019-05-04 12:04:51 +02:00
b3yond fb15771cf2
Merge pull request #93 from ticketfrei/images
Notify that telegram image reports are not supported. #90
2019-05-04 10:23:34 +02:00
sid d1b11fe932
Update active_bots/telegrambot.py
Co-Authored-By: b3yond <b3yond@riseup.net>
2019-05-04 10:22:03 +02:00
b3yond c30f9d8eaa
Merge pull request #87 from ticketfrei/fix-none-error
fixed wrong exception
2019-05-03 17:11:25 +02:00
b3yond 7a7e8f0a30 Notify that telegram image reports are not supported. #90 2019-05-03 14:35:06 +02:00
b3yond e18244e149 don't log Mastodon 502 errors. 2019-05-03 10:07:16 +02:00
b3yond cd3c8be2dc fixed wrong exception 2019-02-19 16:16:21 +01:00
b3yond 02f117a864
Merge pull request #82 from ticketfrei/csrf
Building in CSRF prevention
2019-01-27 17:56:53 +01:00
b3yond 482350f8c7 Merge branch 'csrf' of github:ticketfrei/ticketfrei into csrf 2019-01-27 17:55:23 +01:00
b3yond 6b52a6303a better crypto 2019-01-27 17:53:37 +01:00
b3yond 2a90573d5e cleaning up the code. 2019-01-27 17:39:31 +01:00
b3yond e735936c7a hardened the token and fixed the signature 2019-01-27 16:31:59 +01:00
b3yond ee9b051c71 added CSRF token to settings template 2019-01-27 16:24:58 +01:00
b3yond 139195fd02 added CSRF token to settings template 2019-01-27 16:08:45 +01:00
b3yond 3dd976ef40 This was a weird merge conflict with my own branch o.0 2019-01-27 16:05:53 +01:00
b3yond cdecd170a0 give CSRF token to template engine 2019-01-27 15:56:19 +01:00
b3yond ec68f17b32 write and read CSRF cookie 2019-01-27 15:39:49 +01:00
b3yond ddefc2aafa write and read CSRF cookie 2019-01-27 14:52:42 +01:00
b3yond 60e1d8ec30 found last db.secret and fixed to use the getter 2019-01-27 11:37:21 +01:00
b3yond d5b0ba9b6d removed redundant photo (how did it end up here? I should take a break.) 2019-01-12 01:20:22 +01:00
b3yond 26fa98ad9b Merge branch 'envs' 2019-01-12 01:09:38 +01:00
b3yond de525adb7a Merge branch 'master' of github:b3yond/ticketfrei 2019-01-12 00:34:13 +01:00
b3yond 30c49bbfc8 apparently I didn't find all calls to db.secret 2019-01-12 00:34:03 +01:00
b3yond 880b327b20 new default background image 2019-01-12 00:19:02 +01:00
b3yond 467fdaa42a new default background image 2019-01-12 00:10:55 +01:00
b3yond a4996266a1
Merge pull request #74 from ticketfrei/version-number
Version number
2019-01-11 23:31:25 +01:00
b3yond c9c153117e
Merge pull request #76 from ticketfrei/envs
Use environment variables for config values
2019-01-11 23:25:31 +01:00
b3yond 54489807da no need for such a verbose error message. 2019-01-11 15:16:37 +01:00
b3yond 4b8798ddea fixing shutdown when exim4 is not set up 2019-01-11 14:52:58 +01:00
sid 6a5e7f5028
Merge pull request #75 from ticketfrei/git-sid-patch-1
Update LICENSE
2019-01-11 13:49:16 +01:00
sid 7507d0392d
Update LICENSE 2019-01-11 13:48:29 +01:00
b3yond 4bd99ebb90 updated the issue template 2019-01-11 13:44:27 +01:00
b3yond 12a0b1efe5 added call to GET version (commit hash) 2019-01-11 13:38:47 +01:00
b3yond a38c2316f2
Merge pull request #72 from ticketfrei/confirm-37
check if account already exists to avoid double use of confirmation mail
2019-01-11 13:33:04 +01:00
b3yond 76b3b574f0 replaced attribute with get call 2019-01-11 13:23:37 +01:00
b3yond 2ce27fc52f nicer error messages 2019-01-11 13:21:47 +01:00
b3yond 1c8853341a check if account already exists #37 2019-01-11 12:15:28 +01:00
b3yond a529f4eb23 formatting #70 2019-01-11 11:41:20 +01:00
b3yond 521f0e7ef2
Merge pull request #71 from patcon/patch-1
Add mission to README
2019-01-11 11:39:37 +01:00
Patrick Connolly 2bee67bf84
Add mission to README. 2019-01-07 14:51:37 -05:00
git-sid cb2f3cb2e1 Fix pep8 non-compliant linebreak 2019-01-07 19:05:39 +01:00
git-sid a47ad74619 Replace 3 dots with ellipsis to save space 2019-01-07 19:05:32 +01:00
b3yond f6c19abad6 fixing the original TypeError 2018-12-31 15:33:50 +01:00
b3yond e7e230b2f0 when you get crashes bc of your log messages -. 2018-12-31 15:32:19 +01:00
b3yond e72d4872c0 more verbose telegram error messages 2018-12-31 15:27:11 +01:00
b3yond d5823ee1ad removed redundant table declaration 2018-12-28 14:43:18 +01:00
b3yond 268b9748c3 introduce extra var bc can't write to private attribute 2018-11-12 12:32:28 +01:00
b3yond 8e1234d9b5 removed wrong comment - not only testing, also docker containers use this 2018-11-07 09:22:02 +01:00
b3yond 4c61b1ba99 setting host to 0.0.0.0 - it never worked with smth else anyway 2018-11-07 01:57:47 +01:00
b3yond 5a4763366b if an env var is an empty string, use values from example config 2018-11-06 18:08:51 +01:00
b3yond 945a90c7e1 make config.py output directly applicable 2018-11-06 17:50:57 +01:00
b3yond bc7a4a72f8 beauty overhaul of config.py 2018-11-06 16:23:47 +01:00
b3yond d964927a3f fix small bug, print current config if directly called #64 2018-11-06 16:22:11 +01:00
b3yond 238dd20d20 if no config.toml, set config through environment #64 2018-11-06 16:17:47 +01:00
b3yond f274d25822 updated example config options + 1 little fix 2018-11-06 08:56:24 +01:00
b3yond 710a89c282 fix mailbot crash:
File "/srv/ticketfrei/active_bots/mailbot.py", line 37, in post
    if rec not in report.author:
TypeError: argument of type 'NoneType' is not iterable
2018-10-26 18:20:01 +02:00
b3yond 8b36589557 added 502 to unlogged Telegram error codes 2018-10-26 17:27:00 +02:00
b3yond 7cb211b4cb polishing the wording of RSS subscription 2018-10-26 17:25:25 +02:00
git-sid 9508618347 add rss feed notification option to info page 2018-10-19 08:26:20 +02:00
b3yond 651e684316 add another issue template 2018-10-18 17:09:21 +02:00
b3yond 1a0ae78ac1
Merge pull request #57 from ticketfrei/issue-templates
Update issue templates
2018-10-18 17:06:14 +02:00
b3yond 01f33ea29a Update issue templates 2018-10-18 17:04:06 +02:00
b3yond 400e15d18a fix screenshot links in default city page 2018-10-13 20:01:51 +02:00
b3yond 55db252f44 mastodon seen toots work differently now; function deprecated 2018-10-13 19:34:16 +02:00
b3yond f64142d882 reworked front page text 2018-10-13 19:01:54 +02:00
b3yond f286c127ba brought README.md up to date 2018-10-13 18:56:09 +02:00
b3yond 4428fa932f excepted return message 34 so it doesn't get logged #39 2018-10-11 22:22:37 +02:00
b3yond cc5ab22be5 excepted with wrong Exception 2018-10-11 21:30:55 +02:00
b3yond 56e948b798 called wrong user method 2018-10-11 21:29:02 +02:00
b3yond c36b8ab673 fixing bug; twitterDM object wasn't created 2018-10-11 21:24:53 +02:00
b3yond 17df4f15e4 check if mention is in reply to anything #41 2018-10-08 23:32:33 +02:00
b3yond b5de7cde9f Revert "crawl the username only once from twitter and save to db #45"
This reverts commit 9836ec7752.
2018-10-08 23:27:45 +02:00
b3yond 8eb2d98c03 Merge remote-tracking branch 'origin/master' 2018-10-08 22:14:35 +02:00
b3yond 9836ec7752 crawl the username only once from twitter and save to db #45 2018-10-08 21:31:25 +02:00
b3yond 9e8cfa624c fix repost bug 2018-10-08 21:26:39 +02:00
b3yond 084049bbfe fix repost bug 2018-10-08 15:09:18 +02:00
b3yond 6a8cf5c6af ignore PGP signatures; I hope those messages get posted now #40 2018-10-08 15:02:27 +02:00
b3yond de657ba350 really fix shutdown in #40 2018-10-07 23:28:29 +02:00
b3yond bbe27e2586 fix shutdown in #40 2018-10-07 23:27:06 +02:00
b3yond 9a3c09b119
Merge pull request #50 from ticketfrei/rate-limit-39
missing newlines in /etc/aliases
2018-10-07 23:05:52 +02:00
b3yond 30de2196ac missing newlines in /etc/aliases 2018-10-07 23:01:14 +02:00
b3yond 9ca521493a
Merge pull request #49 from ticketfrei/rate-limit-39
Rate limit 39
2018-10-07 22:19:57 +02:00
b3yond 0449d892a3 insert empty row at account creation 2018-10-07 22:16:00 +02:00
b3yond f59be986e2 reverting #39 - make rate limits per account, not app 2018-10-07 22:10:48 +02:00
b3yond 79d5a6f112 fixed sendmail calls 2018-10-07 21:02:48 +02:00
23 changed files with 326 additions and 133 deletions

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
---
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual Behavior**
A clear and concise description of what happens.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Ticketfrei Version**
See the commit on which Ticketfrei is running at example.org/version.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -0,0 +1,7 @@
---
name: Something else
about: Other ideas?
---
*If your suggestion is neither a bug report nor a feature request, this is the right place. Just describe what you have in mind.*

View file

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

View file

@ -1,10 +1,25 @@
# Ticketfrei social bot
Version: 2.0beta
Ticketfrei is a mastodon/twitter/mail bot to dodge ticket controllers in public
transport systems.
## Mission
Public transportation is meant to provide an easy and time-saving way to move
within a region while being affordable for everybody. Unfortunately, this is
not yet the case. Ticketfrei's approach is to **enable people to 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.
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 could
look like!
## How It Works
The functionality is simple: It retweets every tweet where it is mentioned.
This leads to a community which evolves around it. If you see ticket
@ -13,19 +28,19 @@ your tweet and others can read the info and think twice whether they want to
buy a ticket or not. If enough people, a critical mass, participate for the bot
to become reliable, you have positive self-reinforcing dynamics.
Today, you can use a Twitter, a Mastodon, and Mail with the account. They will
communicate with each other; if someone warns others via Mail, Twitter and
Mastodon users will also see the message. And vice versa.
Today, you can use a Twitter, Mastodon, Telegram, and Mail with the account.
They will communicate with each other; if someone warns others via Mail,
Telegram, Twitter and Mastodon users will also see the message. And vice versa.
In version 2, this bot has received a frontend website. On this website, people
can register an own bot for their city - the website manages multiple bots for
multiple citys. This way, you do not have to host it yourself.
In version 2, this repository contains a web application. On this website,
people can register an own bot for their city - the website manages multiple
bots for multiple citys. This way, you do not have to host it yourself.
In the promotion folder, you'll find some promotion material you can use to
build up such a community in your city. Unfortunately it is in german - but
it's editable, feel free to translate it!
Website: https://ticketfrei.links-tech.org
Website (our flagship instance): https://ticketfrei.links-tech.org
More information: https://wiki.links-tech.org/IT/Ticketfrei
@ -34,9 +49,11 @@ More information: https://wiki.links-tech.org/IT/Ticketfrei
Just go to https://ticketfrei.links-tech.org or another website where this software is
running.
* Register a twitter account
* Register a Mastodon account
* Register on the ticketfrei site
* Optionally: register bots:
* Register a Twitter account
* Register a Mastodon account
* Register a Telegram bot
* Configure account
* The hard part: do the promotion! You need a community.
@ -50,7 +67,7 @@ to check if something was retweeted in the last hour or something.
To this date, we have never heard of this happening though.
### blockisting
### Blocklisting
You also need to edit the goodlist and the blocklist. You can do this on the
website, in the settings of your bot.
@ -70,9 +87,9 @@ a GitHub issue or write to tech@lists.links-tech.org, we are happy to help and s
We wrote these installation notes, so you can set up the website easily:
### Install
### Install from the git repository
To Do:
This guide assumes you are on a Debian 9 Server:
```shell
sudo apt install python3 virtualenv uwsgi uwsgi-plugin-python3 nginx git exim4
@ -91,7 +108,7 @@ virtualenv -p python3 .
Install the dependencies:
```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx gitpython
```
Configure the bot:
@ -101,10 +118,11 @@ cp config.toml.example config.toml
vim config.toml
```
This configuration is only for the admin. Users can log into
This configuration is only for the admin. Moderators can log into
twitter/mastodon/mail and configure their personal bot on the settings page.
Set up LetsEncrypt:
```shell
sudo apt-get install python-certbot-nginx -t stretch-backports
sudo certbot --authenticator webroot --installer nginx --agree-tos --redirect --hsts
@ -176,11 +194,15 @@ less /var/log/syslog
# for the nginx web server:
less /var/log/nginx/example.org_error.log
# for the mail server
less /var/log/exim4/mainlog
```
### Development Install
If you want to install it locally to develop on it:
If you want to install it locally to develop on it, note that twitter and mail
will probably not work. You should test them on a server instead.
```shell
sudo apt install python3 virtualenv uwsgi uwsgi-plugin-python3 nginx git

View file

@ -19,7 +19,11 @@ class Mailbot(Bot):
def crawl(self, user):
reports = []
# todo: adjust to actual mailbox
mails = mailbox.mbox("/var/mail/" + config['mail']['mbox_user'])
try:
mails = mailbox.mbox("/var/mail/" + config['mail']['mbox_user'])
except FileNotFoundError:
logger.error("No mbox file found.")
return reports
for msg in mails:
if get_date_from_header(msg['Date']) > user.get_seen_mail():
if user.get_city().lower() in msg['To'].lower():
@ -34,7 +38,7 @@ class Mailbot(Bot):
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:
if rec not in report.author:
try:
city = user.get_city()
sendmail(rec, "Ticketfrei " + city + " Report",
@ -54,16 +58,19 @@ def make_report(msg, user):
date = get_date_from_header(msg['Date'])
author = msg['From'] # get mail author from email header
# :todo take only the part in between the < >
if msg.is_multipart():
text = []
for part in msg.get_payload():
if part.get_content_type() == "text":
text.append(part.get_payload())
elif part.get_content_type() == "application/pgp-signature":
pass # ignore PGP signatures
elif part.get_content_type() == "multipart/mixed":
for p in part:
if p.get_content_type() == "text":
if isinstance(p, str):
text.append(p)
elif p.get_content_type() == "text":
text.append(part.get_payload())
else:
logger.error("unknown MIMEtype: " +

View file

@ -2,7 +2,7 @@
from bot import Bot
import logging
from mastodon import Mastodon
import mastodon
import re
from report import Report
@ -19,14 +19,14 @@ class MastodonBot(Bot):
"""
mentions = []
try:
m = Mastodon(*user.get_masto_credentials())
m = mastodon.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:
logger.error("Unknown Mastodon API Error.", exc_info=True)
except mastodon.MastodonServerError:
logger.error("Unknown Mastodon API Error: 502")
return mentions
for status in notifications:
if (status['type'] == 'mention' and
@ -54,7 +54,7 @@ class MastodonBot(Bot):
def post(self, user, report):
try:
m = Mastodon(*user.get_masto_credentials())
m = mastodon.Mastodon(*user.get_masto_credentials())
except TypeError:
return # no mastodon account for this user.
if report.source == self:

View file

@ -21,12 +21,23 @@ class TelegramBot(Bot):
return reports
for update in updates:
# return when telegram returns an error code
if update in [303, 404, 420, 500]:
if update in [303, 404, 420, 500, 502]:
return reports
elif isinstance(update, int):
logger.error("Unknown Telegram error code: " + str(update))
if isinstance(update, int):
try:
logger.error("City " + str(user.uid) +
": Unknown Telegram error code: " +
str(update) + " - " + str(updates[1]))
except TypeError:
logger.error("Unknown Telegram error code: " + str(update))
return reports
user.save_seen_tg(update.update_id)
if update.message.photo:
tb.send_message(
update.message.sender.id,
"Sending Photos is not supported for privacy reasons. Can "
"you describe it as text instead?")
continue
if update.message.text.lower() == "/start":
user.add_telegram_subscribers(update.message.sender.id)
tb.send_message(
@ -42,19 +53,24 @@ class TelegramBot(Bot):
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.")
"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))
# set report.author to "" to avoid mailbot crash
sender_name = update.message.sender.username
if sender_name is None:
sender_name = ""
reports.append(Report(sender_name, 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' ...'
text = text[:4096 - 2] + " \N{Horizontal ellipsis}"
try:
for subscriber_id in user.get_telegram_subscribers():
tb.send_message(subscriber_id, text).wait()

View file

@ -5,7 +5,6 @@ import tweepy
import re
import requests
import report
import tfglobals
from time import time
from bot import Bot
@ -13,9 +12,10 @@ from bot import Bot
logger = logging.getLogger(__name__)
class TwitterBot(Bot):
class TwitterDMListener(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
@ -29,19 +29,22 @@ class TwitterBot(Bot):
:return: reports: (list of report.Report objects)
"""
reports = []
if tfglobals.last_twitter_request + 60 > time():
return reports
try:
if user.get_last_twitter_request() + 60 > time():
return reports
except TypeError:
user.set_last_twitter_request(time())
try:
api = self.get_api(user)
except IndexError:
except TypeError:
return reports # no twitter account for this user.
last_dm = user.get_seen_dm()
try:
if last_dm is None:
mentions = api.direct_messages()
else:
mentions = api.mentions_timeline(since_id=last_dm[0])
tfglobals.last_twitter_request = time()
mentions = api.direct_messages(since_id=last_dm[0])
user.set_last_twitter_request(time())
for status in mentions:
text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
@ -59,9 +62,13 @@ class TwitterBot(Bot):
# :todo implement rate limiting
except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection", exc_info=True)
except tweepy.TweepError:
except tweepy.TweepError as terror:
# Waiting for https://github.com/tweepy/tweepy/pull/1109 to get
# merged, so direct messages work again
if terror.api_code == 34:
return reports
logger.error("Twitter API Error: General Error", exc_info=True)
return []
return reports
def post(self, user, report):
pass

View file

@ -7,7 +7,6 @@ import requests
from time import time
import report
from bot import Bot
import tfglobals
logger = logging.getLogger(__name__)
@ -30,9 +29,11 @@ class TwitterBot(Bot):
:return: reports: (list of report.Report objects)
"""
reports = []
#global last_twitter_request
if tfglobals.last_twitter_request + 60 > time():
return reports
try:
if user.get_last_twitter_request() + 60 > time():
return reports
except TypeError:
user.set_last_twitter_request(time())
try:
api = self.get_api(user)
except TypeError:
@ -46,13 +47,12 @@ class TwitterBot(Bot):
mentions = api.mentions_timeline()
else:
mentions = api.mentions_timeline(since_id=last_mention)
tfglobals.last_twitter_request = time()
user.set_last_twitter_request(time())
for status in mentions:
text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
"", status.text)
username = "@" + api.me().screen_name
if username in status.text:
if status._json['in_reply_to_status_id'] == None:
text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
"", status.text)
reports.append(report.Report(status.author.screen_name,
self,
text,
@ -73,7 +73,7 @@ class TwitterBot(Bot):
def post(self, user, report):
try:
api = self.get_api(user)
except IndexError:
except TypeError:
return # no twitter account for this user.
try:
if report.source == self:

View file

@ -5,7 +5,6 @@ from config import config
from db import db
import logging
from sendmail import sendmail
from time import time
def shutdown():
@ -16,8 +15,6 @@ def shutdown():
exit(1)
last_twitter_request = time()
if __name__ == '__main__':
logger = logging.getLogger()
fh = logging.FileHandler('/var/log/ticketfrei/backend.log')

View file

@ -1,5 +1,70 @@
import pytoml as toml
import os
def load_env():
"""
load environment variables from the environment. If empty, use default
values from config.toml.example.
:return: config dictionary of dictionaries.
"""
with open('config.toml.example') as defaultconf:
configdict = toml.load(defaultconf)
try:
if os.environ['CONSUMER_KEY'] != "":
configdict['twitter']['consumer_key'] = os.environ['CONSUMER_KEY']
except KeyError:
pass
try:
if os.environ['CONSUMER_SECRET'] != "":
configdict['twitter']['consumer_secret'] = os.environ['CONSUMER_SECRET']
except KeyError:
pass
try:
if os.environ['HOST'] != "":
configdict['web']['host'] = os.environ['HOST']
except KeyError:
pass
try:
if os.environ['PORT'] != "":
configdict['web']['port'] = os.environ['PORT']
except KeyError:
pass
try:
if os.environ['CONTACT'] != "":
configdict['web']['contact'] = os.environ['CONTACT']
except KeyError:
pass
try:
if os.environ['MBOX_USER'] != "":
configdict['mail']['mbox_user'] = os.environ['MBOX_USER']
except KeyError:
pass
try:
if os.environ['DB_PATH'] != "":
configdict['database']['db_path'] = os.environ['DB_PATH']
except KeyError:
pass
return configdict
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('config.toml') as configfile:
config = toml.load(configfile)
try:
with open('config.toml') as configfile:
config = toml.load(configfile)
except FileNotFoundError:
config = load_env()
if __name__ == "__main__":
for category in config:
for key in config[category]:
print(key + "=" + str(config[category][key]))

View file

@ -10,10 +10,7 @@ port = 80
contact = "b3yond@riseup.net"
[mail]
mailserver = "smtp.riseup.net"
user = "user"
passphrase = "sup3rs3cur3"
mbox = "root"
mbox_user = "root"
[database]
db_path = "/var/ticketfrei/db.sqlite"

34
db.py
View file

@ -14,7 +14,6 @@ class DB(object):
self.conn = sqlite3.connect(dbfile)
self.cur = self.conn.cursor()
self.create()
self.secret = self.get_secret()
def execute(self, *args, **kwargs):
return self.cur.execute(*args, **kwargs)
@ -115,13 +114,6 @@ class DB(object):
FOREIGN KEY(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,
@ -141,6 +133,12 @@ class DB(object):
mail_date REAL,
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 cities (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
@ -190,7 +188,7 @@ class DB(object):
'passhash': scrypt_mcf(
password.encode('utf-8')
).decode('ascii')
}, self.secret).decode('ascii')
}, self.get_secret()).decode('ascii')
def mail_subscription_token(self, email, city):
"""
@ -204,17 +202,17 @@ class DB(object):
token = jwt.encode({
'email': email,
'city': city
}, self.secret).decode('ascii')
}, self.get_secret()).decode('ascii')
return token
def confirm_subscription(self, token):
json = jwt.decode(token, self.secret)
json = jwt.decode(token, self.get_secret())
return json['email'], json['city']
def confirm(self, token, city):
from user import User
try:
json = jwt.decode(token, self.secret)
json = jwt.decode(token, self.get_secret())
except jwt.DecodeError:
return None # invalid token
if 'passhash' in json.keys():
@ -246,17 +244,15 @@ u\d\d?"""
else:
uid = json['uid']
with open("/etc/aliases", "a+") as f:
f.write(city + ": " + config["mail"]["mbox_user"])
f.write(city + ": " + config["mail"]["mbox_user"] + "\n")
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.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.execute("INSERT INTO seen_tweets (user_id, tweet_id) VALUES (?, ?)",
(uid, 0))
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.execute("INSERT INTO seen_tweets (user_id, tweet_id) VALUES (?, ?)", (uid, 0))
self.execute("INSERT INTO twitter_last_request (user_id, date) VALUES (?, ?)", (uid, 0))
self.commit()
user = User(uid)
user.set_city(city)

View file

@ -45,7 +45,7 @@ def register_post():
sendmail(
email,
"Confirm your account",
"Complete your registration here: %s" % (link)
body="Complete your registration here: %s" % (link)
)
return dict(info='Confirmation mail sent.')
except Exception:
@ -56,11 +56,22 @@ def register_post():
@get('/confirm/<city>/<token>')
@view('template/propaganda.tpl')
def confirm(city, token):
# check whether city already exists
if db.by_city(city):
return dict(error='This Account was already confirmed, please try '
'signing in.')
# create db-entry
if db.confirm(token, city):
# :todo show info "Account creation successful."
redirect('/settings')
return dict(error='Email confirmation failed.')
return dict(error='Account creation failed. Please try to register again.')
@get('/version')
def version():
import git
repo = git.Repo(search_parent_directories=True)
return repo.head.object.hexsha
@post('/login')
@ -105,7 +116,7 @@ def subscribe_mail(city):
# 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: " + confirm_link)
city + ", click on this link: " + confirm_link, city=city)
return city_page(city, info="Thanks! You will receive a confirmation mail.")
@ -167,9 +178,10 @@ def register_telegram(user):
return city_page(user.get_city(), info="Thanks for registering Telegram!")
@get('/api/state')
def api_enable(user):
return user.state()
# unused afaik
#@get('/api/state')
#def api_enable(user):
# return user.state()
@get('/static/<filename:path>')
@ -186,6 +198,7 @@ def guides(filename):
def logout():
# clear auth cookie
response.set_cookie('uid', '', expires=0, path="/")
response.set_cookie('csrf', '', expires=0, path="/")
# :todo show info "Logout successful."
redirect('/')
@ -244,11 +257,6 @@ def login_mastodon(user):
try:
access_token = m.log_in(masto_email, masto_pass)
user.save_masto_token(access_token, instance_url)
# 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:
logger.error('Login to Mastodon failed.', exc_info=True)
@ -264,7 +272,6 @@ application = bottle.default_app()
bottle.install(SessionPlugin('/'))
if __name__ == '__main__':
# testing only
bottle.run(host=config["web"]["host"], port=config["web"]["port"])
bottle.run(host="0.0.0.0", port=config["web"]["port"])
else:
application.catchall = False

View file

@ -27,5 +27,5 @@ def sendmail(to, subject, city=None, body=''):
# For testing:
if __name__ == '__main__':
sendmail(config['mail']['contact'], "Test Mail",
sendmail(config['web']['contact'], "Test Mail",
body="This is a test mail.")

View file

@ -1,4 +1,4 @@
from bottle import redirect, request
from bottle import redirect, request, abort, response
from db import db
from functools import wraps
from inspect import Signature
@ -17,10 +17,14 @@ class SessionPlugin(object):
if self.keyword in Signature.from_callable(route.callback).parameters:
@wraps(callback)
def wrapper(*args, **kwargs):
uid = request.get_cookie('uid', secret=db.secret)
uid = request.get_cookie('uid', secret=db.get_secret())
if uid is None:
return redirect(self.loginpage)
kwargs[self.keyword] = User(uid)
if request.method == 'POST':
if request.forms['csrf'] != request.get_cookie('csrf',
secret=db.get_secret()):
abort(400)
return callback(*args, **kwargs)
return wrapper

View file

@ -1,6 +1,6 @@
body {
background-image: url(/static/img/ticketfrei-og-image.jpg);
background-size: 50%;
background-height: 100%;
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 12pt;
line-height: 1.5em;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 628 KiB

View file

@ -15,13 +15,14 @@
your city into a paradise for fare dodgers.
</p>
<p>
Ticketfrei is a Twitter, Mastodon, and E-Mail bot. Users
can help each other by tweeting, tooting, or mailing,
when and where they spot a ticket controller.
Ticketfrei is a Twitter, Mastodon, Telegram, and E-Mail
bot. Users can help each other by tweeting, tooting,
messaging, or mailing, when and where they spot a ticket
controller.
</p>
<p>
Ticketfrei automatically retweets, boosts, and remails
those controller reports, so others can see them. If there
Ticketfrei automatically spreads those controller reports
in the other networks, so others can see them. If there
are ticket controllers around, they can still buy a ticket
- but if the coast is clear, they can save the money.
</p>
@ -31,22 +32,26 @@
to other citys. There are four basic steps:
</p>
<ul>
<li>Create a Twitter and/or a Mastodon account.</li>
<li>Register on this website to create a bot for your city.</li>
<li>Create a Twitter, a Telegram, and/or a Mastodon account.</li>
<li>Log in with the social media accounts you want to
use for Ticketfrei.</li>
<li>Promote the service! Ticketfrei only works if there is
a community for it. Fortunately, we prepared some material
you can use:
<a href="https://github.com/ticketfrei/promotion" target="_blank">https://github.com/ticketfrei/promotion</a></li>
you can edit, remix, use, and republish:
<a href="https://github.com/ticketfrei/promotion" target="_blank">https://github.com/ticketfrei/promotion</a>
<ul>
<li>If you build cool promotion material yourself, please
share it with us, so others can use it, too!</li>
</ul></li>
</ul>
% include('template/register-plain.tpl')
<h2>Our Mission</h2>
<p>
Public transportation is meant to provide an easy and
time-saving way to move within a region while being
affordable for everybody. Unfortunately, this is not the
case. Ticketfrei's approach is to enable people to
affordable for everybody. Unfortunately, this is not yet
the case. Ticketfrei's approach is to enable people to
reclaim public transportation.
</p>
<p>
@ -58,7 +63,7 @@
<p>
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
understanding of what public transportation could look
like!
</p>

View file

@ -61,6 +61,7 @@
<option value='octodon.social'>
<option value='soc.ialis.me'>
</datalist>
<input name='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Log in' type='submit'/>
</form>
</section>
@ -82,6 +83,7 @@
</p>
<form action="/settings/telegram" method="post">
<input type="text" name="apikey" placeholder="Telegram bot API key" id="apikey">
<input name='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Login with Telegram' type='submit'/>
</form>
</div>
@ -106,6 +108,7 @@
</p>
<form action="/settings/markdown" method="post">
<textarea id="markdown" rows="20" cols="70" name="markdown" wrap="physical">{{markdown}}</textarea>
<input name='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Save' type='submit'/>
</form>
</div>
@ -123,6 +126,7 @@
</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='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Save' type='submit'/>
</form>
</div>
@ -137,6 +141,7 @@
</p>
<form action="/settings/goodlist" method="post">
<textarea id="goodlist" rows="8" cols="70" name="goodlist" wrap="physical">{{triggerwords}}</textarea>
<input name='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Submit' type='submit'/>
</form>
</div>
@ -151,6 +156,7 @@
</p>
<form action="/settings/blocklist" method="post">
<textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical">{{badwords}}</textarea>
<input name='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Submit' type='submit'/>
</form>
</div>

View file

@ -1,10 +0,0 @@
from time import time
"""
This file is for shared global variables. They only stay during runtime.
For reference:
https://stackoverflow.com/questions/15959534/visibility-of-global-variables-in-imported-modules
"""
last_twitter_request = time()

45
user.py
View file

@ -1,16 +1,24 @@
from config import config
from bottle import response
from bottle import response, request
from db import db
import jwt
from mastodon import Mastodon
from pylibscrypt import scrypt_mcf, scrypt_mcf_check
from os import urandom
class User(object):
def __init__(self, uid):
# set cookie
response.set_cookie('uid', uid, secret=db.secret, path='/')
response.set_cookie('uid', uid, secret=db.get_secret(), path='/')
self.uid = uid
response.set_cookie('csrf', self.get_csrf(), db.get_secret(), path='/')
def get_csrf(self):
csrf_token = request.get_cookie('csrf', secret=db.get_secret())
if not csrf_token:
csrf_token = str(urandom(32))
return csrf_token
def check_password(self, password):
db.execute("SELECT passhash FROM user WHERE id=?;", (self.uid,))
@ -55,7 +63,7 @@ class User(object):
return jwt.encode({
'email': email,
'uid': self.uid
}, db.secret).decode('ascii')
}, db.get_secret()).decode('ascii')
def is_appropriate(self, report):
db.execute("SELECT patterns FROM triggerpatterns WHERE user_id=?;",
@ -93,6 +101,16 @@ schlitz
return False
return True
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 get_telegram_credentials(self):
db.execute("""SELECT apikey
FROM telegram_accounts
@ -225,6 +243,7 @@ schlitz
# - mail_md
# - goodlist
# - blocklist
# - csrf
# - logged in with twitter?
# - logged in with mastodon?
# - enabled?
@ -234,7 +253,8 @@ schlitz
mail_md=citydict['mail_md'],
triggerwords=self.get_trigger_words(),
badwords=self.get_badwords(),
enabled=self.enabled)
enabled=self.enabled,
csrf=self.get_csrf())
def save_request_token(self, token):
db.execute("""INSERT INTO
@ -290,7 +310,7 @@ schlitz
client_secret = row[1]
return client_id, client_secret
except TypeError:
app_name = "ticketfrei" + str(db.secret)[0:4]
app_name = "ticketfrei" + str(db.get_secret())[0:4]
client_id, client_secret \
= Mastodon.create_app(app_name, api_base_url=instance)
db.execute("""INSERT INTO mastodon_instances(
@ -351,10 +371,13 @@ 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
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)
Benachrichtigungen über E-Mail, Telegram, oder den Mastodon RSS
feed aktivieren. Entweder:
* Gibt hier [deine E-Mail-Adresse an](/city/mail/""" + city + """)
* Subscribe dem Telegram-Bot [@ticketfrei_""" + city + \
"_bot](https://t.me/ticketfrei_" + city + """_bot)
* oder subscribe dem RSS feed von [""" + city + """](""" + masto_link + \
""".atom?replies=false&boosts=true)
Also, wenn du weniger Glück hast, und der erste bist, der einen
Kontrolleur sieht:
@ -378,9 +401,9 @@ mentioned, und gib an
Zum Beispiel so:
![Screenshot of writing a Toot](https://github.com/b3yond/ticketfrei/raw/master/guides/tooting_screenshot.png)
![Screenshot of writing a Toot](https://github.com/b3yond/ticketfrei/raw/stable1/guides/tooting_screenshot.png)
![A toot ready to be shared](https://github.com/b3yond/ticketfrei/raw/master/guides/toot_screenshot.png)
![A toot ready to be shared](https://github.com/b3yond/ticketfrei/raw/stable1/guides/toot_screenshot.png)
Der Bot wird die Nachricht dann weiterverbreiten, auch zu den
anderen Netzwerken.