Compare commits
86 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c778927ee | ||
|
|
079166e74c | ||
|
|
fb15771cf2 | ||
|
|
d1b11fe932 | ||
|
|
c30f9d8eaa | ||
|
|
7a7e8f0a30 | ||
|
|
e18244e149 | ||
|
|
cd3c8be2dc | ||
|
|
02f117a864 | ||
|
|
482350f8c7 | ||
|
|
6b52a6303a | ||
|
|
2a90573d5e | ||
|
|
e735936c7a | ||
|
|
ee9b051c71 | ||
|
|
139195fd02 | ||
|
|
3dd976ef40 | ||
|
|
cdecd170a0 | ||
|
|
ec68f17b32 | ||
|
|
ddefc2aafa | ||
|
|
60e1d8ec30 | ||
|
|
d5b0ba9b6d | ||
|
|
26fa98ad9b | ||
|
|
de525adb7a | ||
|
|
30c49bbfc8 | ||
|
|
880b327b20 | ||
|
|
467fdaa42a | ||
|
|
a4996266a1 | ||
|
|
c9c153117e | ||
|
|
54489807da | ||
|
|
4b8798ddea | ||
|
|
6a5e7f5028 | ||
|
|
7507d0392d | ||
|
|
4bd99ebb90 | ||
|
|
12a0b1efe5 | ||
|
|
a38c2316f2 | ||
|
|
76b3b574f0 | ||
|
|
2ce27fc52f | ||
|
|
1c8853341a | ||
|
|
a529f4eb23 | ||
|
|
521f0e7ef2 | ||
|
|
2bee67bf84 | ||
|
|
cb2f3cb2e1 | ||
|
|
a47ad74619 | ||
|
|
f6c19abad6 | ||
|
|
e7e230b2f0 | ||
|
|
e72d4872c0 | ||
|
|
d5823ee1ad | ||
|
|
268b9748c3 | ||
|
|
8e1234d9b5 | ||
|
|
4c61b1ba99 | ||
|
|
5a4763366b | ||
|
|
945a90c7e1 | ||
|
|
bc7a4a72f8 | ||
|
|
d964927a3f | ||
|
|
238dd20d20 | ||
|
|
f274d25822 | ||
|
|
710a89c282 | ||
|
|
8b36589557 | ||
|
|
7cb211b4cb | ||
|
|
9508618347 | ||
|
|
651e684316 | ||
|
|
1a0ae78ac1 | ||
|
|
01f33ea29a | ||
|
|
400e15d18a | ||
|
|
55db252f44 | ||
|
|
f64142d882 | ||
|
|
f286c127ba | ||
|
|
4428fa932f | ||
|
|
cc5ab22be5 | ||
|
|
56e948b798 | ||
|
|
c36b8ab673 | ||
|
|
17df4f15e4 | ||
|
|
b5de7cde9f | ||
|
|
8eb2d98c03 | ||
|
|
9836ec7752 | ||
|
|
9e8cfa624c | ||
|
|
084049bbfe | ||
|
|
6a8cf5c6af | ||
|
|
de657ba350 | ||
|
|
bbe27e2586 | ||
|
|
9a3c09b119 | ||
|
|
30de2196ac | ||
|
|
9ca521493a | ||
|
|
0449d892a3 | ||
|
|
f59be986e2 | ||
|
|
79d5a6f112 |
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 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
|
||||
|
|
|
|||
56
README.md
56
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: " +
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
69
config.py
69
config.py
|
|
@ -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]))
|
||||
|
|
|
|||
|
|
@ -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
34
db.py
|
|
@ -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)
|
||||
|
|
|
|||
33
frontend.py
33
frontend.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
10
tfglobals.py
10
tfglobals.py
|
|
@ -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
45
user.py
|
|
@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Der Bot wird die Nachricht dann weiterverbreiten, auch zu den
|
||||
anderen Netzwerken.
|
||||
|
|
|
|||
Loading…
Reference in a new issue