commit
9d7cca6a0b
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
7
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal 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.*
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
Copyright (c) 2017 Thomas L <tom@dl6tom.de>
|
Copyright (c) 2017 Thomas L <tom@dl6tom.de>
|
||||||
Copyright (c) 2017 b3yond <b3yond@riseup.net>
|
Copyright (c) 2017 b3yond <b3yond@riseup.net>
|
||||||
Copyright (c) 2018 sid
|
Copyright (c) 2018 sid <sid-sid@riseup.net>
|
||||||
|
|
||||||
Permission to use, copy, modify, and distribute this software for any
|
Permission to use, copy, modify, and distribute this software for any
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
|
19
README.md
19
README.md
|
@ -3,6 +3,23 @@
|
||||||
Ticketfrei is a mastodon/twitter/mail bot to dodge ticket controllers in public
|
Ticketfrei is a mastodon/twitter/mail bot to dodge ticket controllers in public
|
||||||
transport systems.
|
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.
|
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
|
This leads to a community which evolves around it. If you see ticket
|
||||||
|
@ -91,7 +108,7 @@ virtualenv -p python3 .
|
||||||
Install the dependencies:
|
Install the dependencies:
|
||||||
|
|
||||||
```shell
|
```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:
|
Configure the bot:
|
||||||
|
|
|
@ -19,7 +19,11 @@ class Mailbot(Bot):
|
||||||
def crawl(self, user):
|
def crawl(self, user):
|
||||||
reports = []
|
reports = []
|
||||||
# todo: adjust to actual mailbox
|
# todo: adjust to actual mailbox
|
||||||
|
try:
|
||||||
mails = mailbox.mbox("/var/mail/" + config['mail']['mbox_user'])
|
mails = mailbox.mbox("/var/mail/" + config['mail']['mbox_user'])
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("No mbox file found.")
|
||||||
|
return reports
|
||||||
for msg in mails:
|
for msg in mails:
|
||||||
if get_date_from_header(msg['Date']) > user.get_seen_mail():
|
if get_date_from_header(msg['Date']) > user.get_seen_mail():
|
||||||
if user.get_city().lower() in msg['To'].lower():
|
if user.get_city().lower() in msg['To'].lower():
|
||||||
|
|
|
@ -21,9 +21,14 @@ class TelegramBot(Bot):
|
||||||
return reports
|
return reports
|
||||||
for update in updates:
|
for update in updates:
|
||||||
# return when telegram returns an error code
|
# return when telegram returns an error code
|
||||||
if update in [303, 404, 420, 500]:
|
if update in [303, 404, 420, 500, 502]:
|
||||||
return reports
|
return reports
|
||||||
elif isinstance(update, int):
|
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))
|
logger.error("Unknown Telegram error code: " + str(update))
|
||||||
return reports
|
return reports
|
||||||
user.save_seen_tg(update.update_id)
|
user.save_seen_tg(update.update_id)
|
||||||
|
@ -42,19 +47,24 @@ class TelegramBot(Bot):
|
||||||
elif update.message.text.lower() == "/help":
|
elif update.message.text.lower() == "/help":
|
||||||
tb.send_message(
|
tb.send_message(
|
||||||
update.message.sender.id,
|
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
|
# TODO: /help message should be set in frontend
|
||||||
else:
|
else:
|
||||||
reports.append(Report(update.message.sender.username, self,
|
# set report.author to "" to avoid mailbot crash
|
||||||
update.message.text, None,
|
sender_name = update.message.sender.username
|
||||||
update.message.date))
|
if sender_name is None:
|
||||||
|
sender_name = ""
|
||||||
|
|
||||||
|
reports.append(Report(sender_name, self, update.message.text,
|
||||||
|
None, update.message.date))
|
||||||
return reports
|
return reports
|
||||||
|
|
||||||
def post(self, user, report):
|
def post(self, user, report):
|
||||||
tb = Telegram(user.get_telegram_credentials())
|
tb = Telegram(user.get_telegram_credentials())
|
||||||
text = report.text
|
text = report.text
|
||||||
if len(text) > 4096:
|
if len(text) > 4096:
|
||||||
text = text[:4096 - 4] + u' ...'
|
text = text[:4096 - 2] + " \N{Horizontal ellipsis}"
|
||||||
try:
|
try:
|
||||||
for subscriber_id in user.get_telegram_subscribers():
|
for subscriber_id in user.get_telegram_subscribers():
|
||||||
tb.send_message(subscriber_id, text).wait()
|
tb.send_message(subscriber_id, text).wait()
|
||||||
|
|
65
config.py
65
config.py
|
@ -1,5 +1,70 @@
|
||||||
import pytoml as toml
|
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)
|
# read config in TOML format (https://github.com/toml-lang/toml#toml)
|
||||||
|
try:
|
||||||
with open('config.toml') as configfile:
|
with open('config.toml') as configfile:
|
||||||
config = toml.load(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]))
|
||||||
|
|
|
@ -10,10 +10,7 @@ port = 80
|
||||||
contact = "b3yond@riseup.net"
|
contact = "b3yond@riseup.net"
|
||||||
|
|
||||||
[mail]
|
[mail]
|
||||||
mailserver = "smtp.riseup.net"
|
mbox_user = "root"
|
||||||
user = "user"
|
|
||||||
passphrase = "sup3rs3cur3"
|
|
||||||
mbox = "root"
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
db_path = "/var/ticketfrei/db.sqlite"
|
db_path = "/var/ticketfrei/db.sqlite"
|
||||||
|
|
16
db.py
16
db.py
|
@ -14,7 +14,6 @@ class DB(object):
|
||||||
self.conn = sqlite3.connect(dbfile)
|
self.conn = sqlite3.connect(dbfile)
|
||||||
self.cur = self.conn.cursor()
|
self.cur = self.conn.cursor()
|
||||||
self.create()
|
self.create()
|
||||||
self.secret = self.get_secret()
|
|
||||||
|
|
||||||
def execute(self, *args, **kwargs):
|
def execute(self, *args, **kwargs):
|
||||||
return self.cur.execute(*args, **kwargs)
|
return self.cur.execute(*args, **kwargs)
|
||||||
|
@ -115,13 +114,6 @@ class DB(object):
|
||||||
FOREIGN KEY(twitter_accounts_id)
|
FOREIGN KEY(twitter_accounts_id)
|
||||||
REFERENCES twitter_accounts(id)
|
REFERENCES twitter_accounts(id)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS telegram_accounts (
|
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
|
||||||
user_id INTEGER,
|
|
||||||
api_token TEXT,
|
|
||||||
active INTEGER,
|
|
||||||
FOREIGN KEY(user_id) REFERENCES user(id)
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS telegram_subscribers (
|
CREATE TABLE IF NOT EXISTS telegram_subscribers (
|
||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
|
||||||
user_id INTEGER,
|
user_id INTEGER,
|
||||||
|
@ -196,7 +188,7 @@ class DB(object):
|
||||||
'passhash': scrypt_mcf(
|
'passhash': scrypt_mcf(
|
||||||
password.encode('utf-8')
|
password.encode('utf-8')
|
||||||
).decode('ascii')
|
).decode('ascii')
|
||||||
}, self.secret).decode('ascii')
|
}, self.get_secret()).decode('ascii')
|
||||||
|
|
||||||
def mail_subscription_token(self, email, city):
|
def mail_subscription_token(self, email, city):
|
||||||
"""
|
"""
|
||||||
|
@ -210,17 +202,17 @@ class DB(object):
|
||||||
token = jwt.encode({
|
token = jwt.encode({
|
||||||
'email': email,
|
'email': email,
|
||||||
'city': city
|
'city': city
|
||||||
}, self.secret).decode('ascii')
|
}, self.get_secret()).decode('ascii')
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def confirm_subscription(self, token):
|
def confirm_subscription(self, token):
|
||||||
json = jwt.decode(token, self.secret)
|
json = jwt.decode(token, self.get_secret())
|
||||||
return json['email'], json['city']
|
return json['email'], json['city']
|
||||||
|
|
||||||
def confirm(self, token, city):
|
def confirm(self, token, city):
|
||||||
from user import User
|
from user import User
|
||||||
try:
|
try:
|
||||||
json = jwt.decode(token, self.secret)
|
json = jwt.decode(token, self.get_secret())
|
||||||
except jwt.DecodeError:
|
except jwt.DecodeError:
|
||||||
return None # invalid token
|
return None # invalid token
|
||||||
if 'passhash' in json.keys():
|
if 'passhash' in json.keys():
|
||||||
|
|
16
frontend.py
16
frontend.py
|
@ -56,11 +56,22 @@ def register_post():
|
||||||
@get('/confirm/<city>/<token>')
|
@get('/confirm/<city>/<token>')
|
||||||
@view('template/propaganda.tpl')
|
@view('template/propaganda.tpl')
|
||||||
def confirm(city, token):
|
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
|
# create db-entry
|
||||||
if db.confirm(token, city):
|
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='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')
|
@post('/login')
|
||||||
|
@ -259,7 +270,6 @@ application = bottle.default_app()
|
||||||
bottle.install(SessionPlugin('/'))
|
bottle.install(SessionPlugin('/'))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# testing only
|
bottle.run(host="0.0.0.0", port=config["web"]["port"])
|
||||||
bottle.run(host=config["web"]["host"], port=config["web"]["port"])
|
|
||||||
else:
|
else:
|
||||||
application.catchall = False
|
application.catchall = False
|
||||||
|
|
|
@ -27,5 +27,5 @@ def sendmail(to, subject, city=None, body=''):
|
||||||
|
|
||||||
# For testing:
|
# For testing:
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sendmail(config['mail']['contact'], "Test Mail",
|
sendmail(config['web']['contact'], "Test Mail",
|
||||||
body="This is a test mail.")
|
body="This is a test mail.")
|
||||||
|
|
13
user.py
13
user.py
|
@ -361,10 +361,13 @@ Aber je mehr Leute mitmachen, desto eher kannst du dir sicher
|
||||||
sein, dass wir sie finden, bevor sie uns finden.
|
sein, dass wir sie finden, bevor sie uns finden.
|
||||||
|
|
||||||
Wenn du immer direkt gewarnt werden willst, kannst du auch die
|
Wenn du immer direkt gewarnt werden willst, kannst du auch die
|
||||||
Benachrichtigungen über E-Mail oder Telegram aktivieren. Gib
|
Benachrichtigungen über E-Mail, Telegram, oder den Mastodon RSS
|
||||||
einfach <a href="/city/mail/""" + city + """"/">hier</a> deine
|
feed aktivieren. Entweder:
|
||||||
E-Mail-Adresse an oder subscribe dem Telegram-Bot [@ticketfrei_""" + city + \
|
* Gibt hier [deine E-Mail-Adresse an](/city/mail/""" + city + """)
|
||||||
|
* Subscribe dem Telegram-Bot [@ticketfrei_""" + city + \
|
||||||
"_bot](https://t.me/ticketfrei_" + city + """_bot)
|
"_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
|
Also, wenn du weniger Glück hast, und der erste bist, der einen
|
||||||
Kontrolleur sieht:
|
Kontrolleur sieht:
|
||||||
|
@ -388,9 +391,9 @@ mentioned, und gib an
|
||||||
|
|
||||||
Zum Beispiel so:
|
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
|
Der Bot wird die Nachricht dann weiterverbreiten, auch zu den
|
||||||
anderen Netzwerken.
|
anderen Netzwerken.
|
||||||
|
|
Loading…
Reference in a new issue