Compare commits

...

69 Commits

Author SHA1 Message Date
b3yond 7fb70cee57
Merge pull request #48 from b3yond/1.0
1.1
2018-10-07 19:24:50 +02:00
b3yond 05c09c97c6
Merge pull request #34 from b3yond/master
Backporting IMAP error exceptions
2018-09-08 11:29:01 +02:00
b3yond 9e221ed290 Excepted IMAP connection Error 2018-01-30 16:10:33 +01:00
b3yond 0acb89ebf0 excepted IMAP4 error with unknown cause 2018-01-23 09:18:59 +01:00
b3yond c0328be3a4 one \ to much lol 2018-01-19 16:27:30 +01:00
b3yond 4bc9bf9931 final 1.0 2018-01-19 16:13:47 +01:00
b3yond c9e6a35372 updated README to version 1.0. you can disable accounts now 2018-01-19 16:00:36 +01:00
b3yond 04e05ee8ca excepted Mastodon API Error with a too broad exception 2018-01-19 00:17:09 +01:00
b3yond 37b2706a3b excepted TweepError that was raised without an explanation further than 503 2018-01-18 21:48:36 +01:00
b3yond 9305a32eb7 added more save_last(), schadet nicht 2018-01-18 20:15:41 +01:00
b3yond 12fbbde79c bots don't own trigger anymore 2018-01-18 15:18:20 +01:00
b3yond 21e4af6fa9 twitter accidentially crawled too many tweets 2018-01-18 15:14:04 +01:00
b3yond c8e67d1937 better error handling of FileExistsError, fixed regex for mastobot 2018-01-18 15:10:05 +01:00
b3yond 10de40549c added regex magic so twitter & masto don't mention themselves by accident 2018-01-18 14:48:53 +01:00
b3yond ee61ba19e6 mailbot doesn't crawl mails which it wrote itself anymore 2018-01-18 14:23:11 +01:00
b3yond 72d0acb20a bugfix: gave Report.__init__() twitter User object, not screen_name 2018-01-18 13:59:37 +01:00
b3yond b174db3cfe twitterbot.crawl() returns reports now, not statuses 2018-01-18 13:54:32 +01:00
b3yond 048bad181b only send one status at a time 2018-01-18 13:42:23 +01:00
b3yond b5288f341c bugfix: FileExistsError 2018-01-18 13:40:07 +01:00
b3yond 75e1ff902c function needs to take an argument 2018-01-18 13:19:11 +01:00
b3yond d6a0c6d377 changed ticketfrei flow logic, integrated mailbot!!! #11 2018-01-18 13:06:53 +01:00
b3yond cde5494de3 mailbot uses reports now, and doesn't need to own trigger 2018-01-18 12:42:37 +01:00
b3yond ff73c5dc21 Standardized reports; moved flow() logic to crawl(), repost(), & post(); bots don't own Trigger anymore 2018-01-18 11:41:08 +01:00
b3yond 9f060b405e added nice slogan! 2018-01-09 23:01:01 +01:00
b3yond 2f74791dd6 renamed promotion directory on master, too 2018-01-08 00:17:19 +01:00
Thomas L f2a0cf18b4 replace logger class with standard python loggin 2018-01-07 20:22:32 +01:00
Thomas L f0aaa4dc54 remove our api-keys m( 2018-01-07 18:48:35 +01:00
b3yond b9e1b38963 blacklisted certain racist slurs 2018-01-07 01:33:10 +01:00
b3yond 409f9e80f8 exchanged link & QR-Code 2018-01-07 00:52:03 +01:00
b3yond 851992803f created a flyer for autonomous centers 2018-01-06 22:20:36 +01:00
b3yond 79a8965d1c wrote 3 articles for a false-flag-flyer :D 2018-01-06 21:31:23 +01:00
b3yond a0ca940008 wrote fully fleshed out mailbot. has to be connected to ticketfrei.py #11 2018-01-05 17:13:41 +01:00
b3yond 5c98aa7677 started an IMAP listener to implement a 3rd bot: the Mailbot. #11 2018-01-05 14:16:24 +01:00
b3yond 01ad0e1c40 attach logfiles to shutdown mails 2018-01-05 11:20:07 +01:00
b3yond e962bbbe85 removed unused shutdown contact, renamed variable 2018-01-05 11:02:45 +01:00
b3yond 0b89a52da3 log traceback of all unexpected Exceptions 2018-01-05 10:52:15 +01:00
b3yond 31a54fc19f documented log config 2018-01-05 10:43:38 +01:00
b3yond 654af44534 reworked logger class - also handles bot crashes and tbs now. added configline for log directory. 2018-01-05 10:42:31 +01:00
b3yond 8357be7f7d typo 2018-01-04 12:23:41 +01:00
b3yond 98dd5e4212 improved the traceback messages 2018-01-04 12:20:59 +01:00
b3yond aa45a8e814 small fix 2018-01-04 11:05:36 +01:00
b3yond 0f6fc60b5e crash reports are now sent via mail. documented config.toml.example 2018-01-04 11:02:42 +01:00
b3yond df32f3c614 added class to write mails to users 2018-01-01 11:23:50 +01:00
b3yond d7dea7df00 finished changes to class structure 2017-12-30 16:33:34 +01:00
b3yond 96ef5e2a3f typo 2017-12-30 16:23:53 +01:00
b3yond e64e3702f6 moved log to own class 2017-12-30 16:20:25 +01:00
Thomas L 594b3fb5de fix fd mode 2017-12-30 11:31:16 +01:00
b3yond d22c85da1b Renamed config file to config.toml #6 2017-12-30 10:32:20 +01:00
b3yond 4aa4846527 optimized install docs 2017-12-30 01:21:57 +01:00
b3yond 7a1a857ab4 added documentation -> python3 #7 2017-12-30 01:17:13 +01:00
b3yond cbf16b8f74 changed ticketfrei.py to python3 #7 2017-12-30 01:15:22 +01:00
b3yond fb24c758a8 changed twitterbot to python3 + tweepy #7 2017-12-30 01:11:28 +01:00
b3yond 15d2c75b5a Merge branch 'master' of https://github.com/b3yond/ticketfrei 2017-12-10 20:20:39 +01:00
b3yond 2c21fb09ca added todo 2017-12-10 20:20:30 +01:00
b3yond 42aa60a968 wrote documentation 2017-11-28 15:11:09 +01:00
b3yond a4eef4b086 updated gitignore 2017-11-24 18:16:38 +01:00
b3yond 9e38906898 new image 2017-11-24 18:15:56 +01:00
b3yond b1348e5578 Merge branch 'master' of https://github.com/b3yond/ticketfrei 2017-11-24 18:13:52 +01:00
b3yond 1ee464cf97 patc designed a more readable sticker :D 2017-11-01 23:10:40 +01:00
b3yond 3ee52532d2 added nbg_ticketfrei logo 2017-10-18 19:15:16 +02:00
b3yond 357d6c4fc2 would be a nice feature 2017-10-17 15:29:09 +02:00
b3yond 36f919826f Merge branch 'master' of https://github.com/b3yond/ticketfrei 2017-10-17 15:27:56 +02:00
b3yond ee256af154 advice about using screen 2017-10-17 15:27:24 +02:00
b3yond 694a930d73 Merge branch 'master' of dl6tom.de:public/ticketfrei 2017-10-17 00:16:51 +02:00
b3yond d6a94432c8 added another todo point 2017-10-17 00:15:56 +02:00
Thomas L aefe78eb50 add license 2017-10-17 00:14:57 +02:00
b3yond 150e3579b7 added 2 todo points 2017-10-17 00:04:21 +02:00
ng0 f4b8300ac1
minor correction to ticketfrei.cfg.example 2017-10-14 19:54:57 +00:00
b3yond 50f81c3bc1 invented a campaign 2017-10-11 22:22:53 +02:00
27 changed files with 920 additions and 306 deletions

5
.gitignore vendored
View File

@ -1,12 +1,17 @@
*.swp
*.pyc
.idea/
__pycache__/
last_mention
last_mail
ticketfrei.cfg
ticketfrei.sqlite
seen_toots.pickle
seen_toots.pickle.part
pip-selfcheck.json
config.toml
bin/
include/
lib/
share/
local/

14
LICENSE Normal file
View File

@ -0,0 +1,14 @@
Copyright (c) 2017 Thomas L <tom@dl6tom.de>
Copyright (c) 2017 b3yond <b3yond@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
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

110
README.md
View File

@ -1,68 +1,96 @@
# Ticketfrei micro messaging bot
Version: 1.0
<!-- This mastodon/twitter bot has one purpose - breaking the law. -->
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 controllers, you tweet their location and mention the bot. The bot then retweets your tweet and others can read the info and think twice if they want to buy a ticket. If enough people, a critical mass, participate for the bot to become reliable, you have positive self-reinforcing dynamics.
This leads to a community which evolves around it; if you see ticket
controllers, you tweet their location and mention the bot. The bot
then retweets your tweet and others can read the info and think twice
if they want to buy a ticket. If enough people, a critical mass,
participate for the bot to become reliable, you have positive
self-reinforcing dynamics.
There is one security hole: people could start mentioning the bot with useless information, turning it into a spammer. That's why it has to be maintained; if someone spams the bot, mute them and undo the retweet. So it won't retweet their future tweets and the useless retweet is deleted if someone tries to check if something was retweeted in the last hour or something.
In the promotion folder, you will find some promotion material you
can use to build up such a community in your city. It is in german
though =/
Website: https://wiki.links-it.de/IT/Ticketfrei
Website: https://wiki.links-tech.org/IT/Ticketfrei
# Install
## Install
Install python and virtualenv with your favourite package manager.
Create and activate virtualenv
Setting up a ticketfrei bot for your city is quite easy. Here are the
few steps:
First you need to install python3 and virtualenv with your favourite
package manager.
Create and activate virtualenv:
```shell
$ virtualenv -p python2 .
$ . bin/activate
sudo apt install python3 virtualenv
virtualenv -p python3 .
. bin/activate
```
Install dependencies
Install the dependencies:
```shell
$ pip install python-twitter pytoml requests Mastodon.py
pip install tweepy pytoml requests Mastodon.py
```
Configure
Configure the bot:
```shell
$ cp ticketfrei.cfg.example ticketfrei.cfg
$ vim ticketfrei.cfg
cp config.toml.example config.toml
vim config.toml
```
Edit the account credentials, so your bot can use your accounts.
Also add the words to the goodlist, which you want to require. A tweet is only retweeted, if it contains at least one of them. If you want to RT everything, just add your account name.
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.
There is also a blacklist, which you can use to automatically sort out malicious tweets. Be careful though, our filter can't read the intention with which a word was used. Maybe you wanted it there.
You have to configure all of the accounts via config.toml; it should
be fairly intuitive to enter the right values.
Note that atm the good- & blacklist are still outside of ticketfrei.cfg, in separate files. we will repare this soon.
## Maintaining
To keep the bots running when you are logged out of the shell, you can use screen:
There is one security hole: people could start mentioning the bot
with useless information, turning it into a spammer. That's why it
has to be maintained; if someone spams the bot, mute them and undo
the retweet. So it won't retweet their future tweets and the useless
retweet is deleted if someone tries to check if something was
retweeted in the last hour or something.
To this date, we have never heard of this happening though.
### blacklisting
You also need to edit the goodlist and the blacklist. They are in the
"goodlists" and "blacklists" folders. All text files in those
directories will be used, so you should delete our templates; but
feel free to use them as an orientation.
Just add the words to the goodlist, which you want to require. A
report is only spread, if it contains at least one of them. If you
want to RT everything, just add a ```*```.
There is also a blacklist, which you can use to automatically sort
out malicious tweets. Be careful though, our filter can't read the
intention with which a word was used. Maybe you wanted it there.
### screen
To keep the bots running when you are logged out of the shell, you
can use screen:
```shell
sudo apt-get install screen
echo "if [ -z "$STY" ]; then screen -RR; fi" >> ~/.bash_login
screen
python ticketfrei.py
python3 ticketfrei.py
```
## ideas
To log out of the screen session, press "ctrl+a", and then "d".
* You can only use the twitter API if you have confirmed a phone number and sacrificed a penguin in a blood ritual. So we should build it in a way that it uses the twitter web GUI. It's difficult, but maybe it works. We had another twitter bot that worked similarly, years ago: https://github.com/b3yond/twitter-bot
* Build a tool that deletes wrong toots/tweets on both platforms, would work nicely with a web UI.
* write the muted people to the db, to easily undo the mutes if necessary.
## to do
Desktop/pycharm-community-2017.1.4/bin/pycharm.sh
- [x] Twitter: Crawl mentions
- [x] Mastodon: Crawl mentions
- [ ] Write toots/tweets to database/log
- [x] Twitter: retweet people
- [x] Mastodon: boost people
- [x] Twitter: access the API
- [ ] Web UI that lets you easily delete toots/tweets per db id and mute the tweet author
- [x] Write Bots as Classes to be easier implemented
- [x] Create extra Class for the filter
- [x] Put as much as possible into ticketfrei.cfg
- [x] Make both bots run on their own *and* next to each other
- [x] implement trigger class in retootbot
- [x] read config in retweetbot
- [x] put shutdown contact in ticketfrei.cfg

View File

@ -8,3 +8,6 @@ jude
schwuchtel
fag
faggot
nigger
neger
schlitz

48
config.toml.example Normal file
View File

@ -0,0 +1,48 @@
[mapp]
# The bot registers a mastodon app automatically to acquire OAuth keys.
name = 'yourcity_ticketfrei' # What you want the app to be called
[muser]
enabled = 'false' # set to true if you want to use Mastodon
email = 'youremail@server.tld' # E-mail address of your Mastodon account
password = 'yourpassword' # Password of your Mastodon account
server = 'yourmastodoninstance' # Instance where you have your Mastodon account
[tapp]
# You get those keys when you follow these steps:
# https://developer.twitter.com/en/docs/basics/authentication/guides/access-tokens
consumer_key = "your_consumer_key"
consumer_secret = "your_consumer_secret"
[tuser]
enabled = 'false' # set to true if you want to use Twitter
# You get those keys when you follow these steps:
# https://developer.twitter.com/en/docs/basics/authentication/guides/access-tokens
access_token_key = "your_access_token_key"
access_token_secret = "your_acces_token_secret"
[mail]
enabled = 'false' # set to true if you want to use Mail notifications
# This is the mail the bot uses to send emails.
mailserver = "smtp.riseup.net"
user = "ticketfrei"
passphrase = "sup3rs3cur3"
# If you want to receive crash reports (so you can restart the bot
# when it breaks down), you should specify a contact email address:
#contact = "your_mail@riseup.net"
# Mailing list where you want to send warnings to
list = "yourcity_ticketfrei@lists.links-tech.org"
[logging]
# The directory where logs should be stored.
logpath = "logs/ticketfrei.log"
# [trigger]
# goodlists are one regex per line.
# badlists are one badword per line.
# a message musst match at least one regex in goodlist and contain none of the badwords.
# the variables mention the directory where the lists are located, not the filenames.
# These are the default folders. If you want to specify differents folders, uncomment
# those lines and enter relative paths.
#goodlist_path = 'goodlists'
#blacklist_path = 'blacklists'

1
local

@ -1 +0,0 @@
Subproject commit c8e9d7fd7ae0fe04921fdcf85e11fc9c0c324958

211
mailbot.py Normal file
View File

@ -0,0 +1,211 @@
#!/usr/bin/env python3
import sendmail
import ssl
import time
import trigger
import datetime
import email
import logging
import pytoml as toml
import imaplib
import report
logger = logging.getLogger(__name__)
class Mailbot(object):
"""
Bot which sends Mails if mentioned via twitter/mastodon, and tells
other bots that it received mails.
"""
def __init__(self, config, history_path="last_mail"):
"""
Creates a Bot who listens to mails and forwards them to other
bots.
:param config: (dictionary) config.toml as a dictionary of dictionaries
"""
self.config = config
self.history_path = history_path
self.last_mail = self.get_history(self.history_path)
try:
self.mailinglist = self.config["mail"]["list"]
except KeyError:
self.mailinglist = None
self.mailbox = imaplib.IMAP4_SSL(self.config["mail"]["imapserver"])
context = ssl.create_default_context()
try:
self.mailbox.starttls(ssl_context=context)
except:
logger.error('StartTLS failed', exc_info=True)
try:
self.mailbox.login(self.config["mail"]["user"], self.config["mail"]["passphrase"])
except imaplib.IMAP4.error:
logger.error("Login to mail server failed", exc_info=True)
try:
mailer = sendmail.Mailer(config)
mailer.send('', config['mail']['contact'],
'Ticketfrei Crash Report',
attachment=config['logging']['logpath'])
except:
logger.error('Mail sending failed', exc_info=True)
def repost(self, status):
"""
E-Mails don't have to be reposted - they already reached everyone on the mailing list.
The function still needs to be here because ticketfrei.py assumes it, and take the
report object they want to give us.
:param status: (report.Report object)
"""
pass
def crawl(self):
"""
crawl for new mails.
:return: msgs: (list of report.Report objects)
"""
try:
rv, data = self.mailbox.select("Inbox")
except imaplib.IMAP4.abort:
rv = "Crawling Mail failed"
logger.error(rv, exc_info=True)
except TimeoutError:
rv = "No Connection"
logger.error(rv, exc_info=True)
msgs = []
if rv == 'OK':
rv, data = self.mailbox.search(None, "ALL")
if rv != 'OK':
return msgs
for num in data[0].split():
rv, data = self.mailbox.fetch(num, '(RFC822)')
if rv != 'OK':
logger.error("Couldn't fetch mail %s %s" % (rv, str(data)))
return msgs
msg = email.message_from_bytes(data[0][1])
if not self.config['mail']['user'] + "@" + \
self.config["mail"]["mailserver"].partition(".")[2] in msg['From']:
# get a comparable date out of the email
date_tuple = email.utils.parsedate_tz(msg['Date'])
date_tuple = datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
date = (date_tuple - datetime.datetime(1970, 1, 1)).total_seconds()
if date > self.get_history(self.history_path):
self.last_mail = date
self.save_last()
msgs.append(self.make_report(msg))
return msgs
def get_history(self, path):
"""
This counter is needed to keep track of your mails, so you
don't double parse them
:param path: string: contains path to the file where the ID of the
last_mail is stored.
:return: last_mail: ID of the last mail the bot parsed
"""
try:
with open(path, "r+") as f:
last_mail = f.read()
except IOError:
with open(path, "w+") as f:
last_mail = "0"
f.write(last_mail)
return float(last_mail)
def save_last(self):
""" Saves the last retweeted tweet in last_mention. """
with open(self.history_path, "w") as f:
f.write(str(self.last_mail))
def post(self, status):
"""
sends reports by other sources to a mailing list.
:param status: (report.Report object)
"""
mailer = sendmail.Mailer(self.config)
mailer.send(status.format(), self.mailinglist, "Warnung: Kontrolleure gesehen")
def make_report(self, msg):
"""
generates a report out of a mail
:param msg: email.parser.Message object
:return: post: report.Report object
"""
# get a comparable date out of the email
date_tuple = email.utils.parsedate_tz(msg['Date'])
date_tuple = datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
date = (date_tuple-datetime.datetime(1970,1,1)).total_seconds()
author = msg.get("From") # get mail author from email header
# :todo take only the part before the @
text = msg.get_payload()
post = report.Report(author, "mail", text, None, date)
self.last_mail = date
self.save_last()
return post
def flow(self, trigger, statuses):
"""
to be iterated. uses trigger to separate the sheep from the goats
:param statuses: (list of report.Report objects)
:return: statuses: (list of report.Report objects)
"""
for status in statuses:
self.post(status)
msgs = self.crawl()
statuses = []
for msg in msgs:
if trigger.is_ok(msg.get_payload()):
statuses.append(msg)
return statuses
if __name__ == "__main__":
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('config.toml') as configfile:
config = toml.load(configfile)
# set log file
logger = logging.getLogger()
fh = logging.FileHandler(config['logging']['logpath'])
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
# initialise trigger
trigger = trigger.Trigger(config)
# initialise mail bot
m = Mailbot(config)
statuses = []
try:
while 1:
print("Received Reports: " + str(m.flow(trigger, statuses)))
time.sleep(1)
except KeyboardInterrupt:
print("Good bye. Remember to restart the bot!")
except:
logger.error('Shutdown', exc_info=True)
m.save_last()
try:
mailer = sendmail.Mailer(config)
mailer.send('', config['mail']['contact'],
'Ticketfrei Crash Report',
attachment=config['logging']['logpath'])
except:
logger.error('Mail sending failed', exc_info=True)

45
promotion/campaign.md Normal file
View File

@ -0,0 +1,45 @@
# Campaign to build a local community around ticketfrei
## Target groups
Students: usually already have a ticket, but may be solidaric
* especially design university
Leftist scene
* Flyers in alternative centers
* Graffitis in alternative neighbourhoods
Schools:
* especially trade schools
Nightlife
* Spread flyers in bars and nightclubs
Fare Dodger
* ppl in the queue of the Service Center
## Material
Logo + Header Picture
Sticker
Flyer
* 1 Flyer in high-polish VAG-Layout
* 1 Flyer in DIY-Anarcho-Style
Graffiti stencils
## Video
2-3 Minutes explaining video with Anonymous-Mask und cryptical language
* Have fun with a greenscreen - a mask, floating through a populated subway
Short video how to set up your own Ticketfrei bot
## Talk in alternative centers
talk, maybe 15 minutes
* How does the bot work?
* Why is this politically relevant, what is sousveillance?
* What is Ticketfreier ÖPNV & why is it good for everyone?

View File

@ -1,3 +1,5 @@
Flyer, der bestimmt gut an Universitäten geht:
# Ticketfrei fahren? Nur wenn wir zusammenhelfen!
Das Problem sind immer die Kontrolleure.

148
promotion/flyer_polite.md Normal file
View File

@ -0,0 +1,148 @@
# Neues Konzept für ein Sozialticket - wie können auch sie Ticketfrei fahren?
Die VAG stellt ein neues Konzept als Alternative zu unerwünschten
Fahrkartenkontrollen vor. Immer mehr Beschwerden über aufdringliche
Kontrolleure und Kontrollerinnen erreichen uns; und weil wir Wert auf
Userfreundlichkeit legen, haben wir ein neues Konzept entwickelt, das
Ihnen helfen soll, Fahrkartenkontrollen zu vermeiden. Ganz im Sinne
der Digitalisierung nutzen wir Soziale Medien, um Ihr Leben zu
erleichtern - ein Twitterbot soll Ihnen dabei helfen, Kontrollen
zu umgehen.
Sie als Fahrgast können davon profitieren, indem Sie einen Blick auf
das @nbg_ticketfrei-Twitterprofil werfen. Dort tragen andere User und
Userinnen Informationen darüber zusammen, wo gerade Kontrolleure
unterwegs sind.
Wenn auf einer Linie, die Sie benutzen wollen, gerade Kontrolleure
und Kontrolleurinnen gesichtet wurden, kaufen Sie besser eine
Fahrkarte. Wenn die Luft aber rein ist, können Sie sich die Kosten
sparen und unser Angebot auch ohne Ticket nutzen. Wenn Sie viel
unterwegs sind, sollten sie auch in Erwägung ziehen, unsere
E-Mail-Notifications zu abonnieren.
Wenn Sie als User oder Userin zu dem Konzept beitragen wollen, geht
das ganz einfach - immer, wenn Sie Kontrollpersonen bei ihrer Arbeit
sehen, schreiben Sie einfach einen Tweet an @nbg_ticketfrei, wo und
in welcher Richtung diese gerade unterwegs sind. Unser Bot retweetet
das dann, damit auch alle anderen über die Bedrohung Bescheid wissen.
Damit wollen wir dass ÖPNV endlich für alle möglich wird. Zu viele
Menschen können sich VAG-Tickets leider nicht leisten - gerade die,
die ihre MobiCard am Ende des Monats kaufen, müssen oft zwischen der
MobiCard und dem letzten Wocheneinkauf abwägen. Damit endlich alle
Menschen in Nürnberg/Fürth/Erlangen U-Bahn fahren können, soll
@nbg_ticketfrei ihnen helfen, Kontrollen zu vermeiden.
Weitere Informationen, wie Sie mitmachen können, finden Sie unter
[Wiki-Page auf Deutsch, Englisch, Türkisch, Russisch, Spanisch]
[QR-Code]
# Was sagen die Menschen dazu?
Melina Moliescu, KFZ-Mechanikerin (28): "Bevor es Ticketfrei gab,
war es immer ein Glücksspiel, ohne Ticket mit der VAG unterwegs zu
sein. Ich musste einen großen Teil meines Lohns entweder für teure
Einzelfahrkarten ausgeben oder für Bußgelder - wenn ich mal kein
Glück hatte. Jetzt sehe ich fast immer, wann ich mir ein Ticket
kaufen sollte, und wann ich es mir sparen kann. Durch das neue
Ticketfrei-System habe ich endlich genug Geld auf die Seite legen
können, um mal wieder in den Urlaub zu fliegen!"
Adolf Eichmann, Kontrolleur (45): "Dank Ticketfrei komme ich nun viel
seltener in die unangenehme Situation, Menschen wegen eines fehlenden
Fahrscheins belästigen zu müssen. Das macht meinen Job bedeutend
entspannter! Ich kontrolliere jetzt wirklich nur noch Leute, die auch
kontrolliert werden wollen. Niemand spuckt mich in der U-Bahn mehr an
oder beleidigt mich. Meine Familie findet auch, dass ich Abends viel
relaxter bin als früher, meine Tochter fängt langsam wieder an,
Vertrauen zu mir zu fassen."
Tick, Trick & Track, Schüler (alle 12): "In unsere Schülertickets
müssen wir jeden Monat die neue Monatskarte einlegen und eine
sechsstellige Nummer abschreiben - unnötig kompliziert, wir
vergessen das jedes Mal. Und unsere Freunde und Freundinnen in
Erlangen können wir damit auch nicht besuchen. Mit Ticketfrei
kann man die Kontolleure und Kontrolleurinnen weitläufig umgehen,
statt plötzlich hastig davonwatscheln zu müssen."
Mehmet Müller, Kindergärtner (34): "Ich finde es schade, dass nicht
alle Menschen sich U-Bahnen leisten können. Mit meinem sozialen
Beruf wäre das auch schwierig, zum Glück verdient meine Frau genug,
darum gönnen wir uns dieses Privileg. Aber ich sehe nicht ein, warum
man die U-Bahn dann nicht auch voll packen soll! Bringt doch nichts,
wenn manche auf dem Bahnsteig stehen bleiben und laufen müssen, wenn
noch Platz in der Bahn wäre. Sie fährt doch bereits, ich und die
anderen haben ja bezahlt. Warum sollen die, die nicht bezahlen
können, nicht mitfahren?"
## Slogan für 1 Werbeanzeige oder so:
WENN Sie Ihre Ziele jeden Tag risikolos & kostenfrei erreichen,
DANN nur weil sie den Ticketfrei-Service der VGN nutzen und nicht
auf gut Glück schwarzfahren.
WENN
HEIMAT scheiße ist,
DANN ist
SÖDER dafür Verantwortlich
(eine Anzeige von Ihrem Heimatministerium und verehrten Ministerpräsidenten (fast))
WENN du beim
SCHWARZFAHREN erwischt wirst,
DANN nur weil Du noch nicht den Ticketfrei-Service deiner
VGN nutzt.
WENN
ÜBERWACHUNG sich lohnt,
DANN weil die
VAG öffentlich ist
# Eine Zukunft ohne Ticketautomaten und Kontrolleure
Ticketfrei ist nicht nur ein Ansatz für Userfreundlichkeit, es ist
gleichzeitig ein Modellprojekt für die Zukunft. Ticketfreier ÖPNV
ist ein unkomplizierter Ansatz, Mobilität für alle zu finanzieren.
Dabei werden Busse und Bahnen vollständig aus Steuern bezahlt. Das
derzeitige Ticketsystem ist unnötig kompliziert - oft ist es unklar,
welche Tickets wo gelten, wie lange, und was man für sein Geld
bekommt. Wenn der ÖPNV komplett durch Steuern finanziert wäre,
bräuchte man keine Kontrolleure und Kontrolleurinnen, keine Automaten
und deren Wartung mehr zu finanzieren - und Ihr Geld wird direkt
dafür benutzt, Sie und alle anderen von A nach B zu bringen.
Wenn man die U-Bahn sozusagen bereits bezahlt hat, gibt es auch
weniger Gründe, im Alltag noch mit dem Auto zu fahren. Es ist nicht
nur gut für die Umwelt, wenn mehr Leute die Bahnen nutzen. Es
entlastet auch die Innenstädte, weniger Smog, Lärm, Hektik, und
weniger Verkehrsunfälle tragen zu einem guten Miteinander bei.
Das ist auch für Leute aus dem Umland gut: kein Verkehrschaos und
keine nervenaufreibende Parkplatzsuche in der Innenstadt mehr. Man
kann einfach am Stadtrand parken und entspannt die U-Bahn nehmen, um
seine Einkäufe zu tätigen und Verwandte zu besuchen.
Für Touristen bedeutet der ticketfreie ÖPNV eine massive
Erleichterung. In einer fremden Stadt herauszufinden, welche
Preisstufe man bezahlen muss, ist für viele schwierig. Gerade wenn
man kein Deutsch spricht, ist das komplexe System der Tarifzonen,
Kurzstrecken, und Gültigkeitsdauer schwer zu begreifen. Mit dem
falschen Ticket erwischt zu werden, kann einem den Urlaub schon mal
versauen.
Stattdessen kann Nürnberg als die Stadt bekannt werden, in der
U-Bahnen einen kostenlos überall hin mitnehmen. Tourismuseinnahmen
kommen der breiten Bevölkerung zu Gute. Neben den fahrerlosen
U-Bahnen, dem Doku-Zentrum, der Burg, den Nürnberger Prozessen und
natürlich dem Christkindelsmarkt kann ticketfreier ÖPNV dazu
beitragen, Nürnberg als Weltstadt bekannt zu machen - hier wird
nicht nur Geschichte, sondern auch die Zukunft geschrieben.
Denn in der Zukunft fahren die U-Bahnen nicht nur fahrerlos, sondern
auch ohne Tickets!

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

BIN
promotion/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

BIN
promotion/sex_pistols.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

BIN
promotion/vag.xcf Normal file

Binary file not shown.

BIN
promotion/wallpaper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

36
report.py Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
class Report(object):
"""
A ticketfrei report object.
Toots, Tweets, and E-Mails can be formed into ticketfrei reports.
"""
def __init__(self, author, source, text, id, timestamp):
"""
Constructor of a ticketfrei report
:param author: username of the author
:param source: mastodon, twitter, or email
:param text: the text of the report
:param id: id in the network
:param timestamp: time of the report
"""
self.author = author
self.type = source
self.text = text
self.timestamp = timestamp
self.id = id
def format(self):
"""
Format the report for bot.post()
:rtype: string
:return: toot: text to be tooted, e.g. "_b3yond: There are
uniformed controllers in the U2 at Opernhaus."
"""
strng = self.author + ": " + self.text
return strng

197
retootbot.py Normal file → Executable file
View File

@ -6,17 +6,19 @@ import os
import pickle
import re
import time
import datetime
import trigger
import traceback
import logging
import sendmail
import report
logger = logging.getLogger(__name__)
class RetootBot(object):
def __init__(self, config, filter, logpath=None):
def __init__(self, config):
self.config = config
self.filter = filter
self.register()
self.login()
self.client_id = self.register()
self.m = self.login()
# load state
try:
@ -25,85 +27,102 @@ class RetootBot(object):
except IOError:
self.seen_toots = set()
if logpath:
self.logpath = logpath
else:
self.logpath = os.path.join("logs", str(datetime.datetime.now()))
def log(self, message, tb=False):
"""
Writing an error message to a logfile in logs/ and prints it.
:param message(string): Log message to be displayed
:param tb: String of the Traceback
"""
time = str(datetime.datetime.now())
if tb:
message = message + " The traceback is located at " + os.path.join("logs" + time)
with open(os.path.join("logs", time), 'w+') as f:
f.write(tb)
line = "[" + time + "] "+ message + "\n"
with open(self.logpath, 'a') as f:
try:
f.write(line)
except UnicodeEncodeError:
self.log("Failed to save log message due to UTF-8 error. ")
traceback.print_exc()
print line,
def register(self):
self.client_id = os.path.join(
'appkeys',
self.config['mapp']['name'] +
'@' + self.config['muser']['server']
)
client_id = os.path.join(
'appkeys',
self.config['mapp']['name'] +
'@' + self.config['muser']['server']
)
if not os.path.isfile(self.client_id):
if not os.path.isfile(client_id):
mastodon.Mastodon.create_app(
self.config['mapp']['name'],
api_base_url=self.config['muser']['server'],
to_file=self.client_id
)
self.config['mapp']['name'],
api_base_url=self.config['muser']['server'],
to_file=client_id
)
return client_id
def login(self):
self.m = mastodon.Mastodon(
client_id=self.client_id,
api_base_url=self.config['muser']['server']
)
self.m.log_in(
self.config['muser']['email'],
self.config['muser']['password']
)
m = mastodon.Mastodon(
client_id=self.client_id,
api_base_url=self.config['muser']['server']
)
m.log_in(
self.config['muser']['email'],
self.config['muser']['password']
)
return m
def retoot(self, toots=()):
def save_last(self):
""" save the last seen toot """
try:
with os.fdopen(os.open('seen_toots.pickle.part', os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'wb') as f:
pickle.dump(self.seen_toots, f)
except FileExistsError:
os.unlink('seen_toots.pickle.part')
with os.fdopen(os.open('seen_toots.pickle.part', os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'wb') as f:
pickle.dump(self.seen_toots, f)
os.rename('seen_toots.pickle.part', 'seen_toots.pickle')
def crawl(self):
"""
Crawl mentions from Mastodon.
:return: list of statuses
"""
mentions = []
try:
all = self.m.notifications()
except: # mastodon.Mastodon.MastodonAPIError is unfortunately not in __init__.py
logger.error("Unknown Mastodon API Error.", exc_info=True)
return mentions
for status in all:
if (status['type'] == 'mention' and status['status']['id'] not in self.seen_toots):
# save state
self.seen_toots.add(status['status']['id'])
self.save_last()
# add mention to mentions
text = re.sub(r'<[^>]*>', '', status['status']['content'])
text = re.sub("(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)", "", text)
mentions.append(report.Report(status['account']['acct'],
"mastodon",
text,
status['status']['id'],
status['status']['created_at']))
return mentions
def repost(self, mention):
"""
Retoots a mention.
:param mention: (report.Report object)
"""
logger.info('Boosting toot from %s' % (
mention.format()))
self.m.status_reblog(mention.id)
def post(self, report):
"""
Toots a report from other sources.
:param report: (report.Report object)
"""
toot = report.format()
self.m.toot(toot)
def flow(self, trigger, reports=()):
# toot external provided messages
for toot in toots:
self.m.toot(toot)
for report in reports:
self.post(report)
# boost mentions
retoots = []
for notification in self.m.notifications():
if (notification['type'] == 'mention'
and notification['status']['id'] not in self.seen_toots):
self.seen_toots.add(notification['status']['id'])
text_content = re.sub(r'<[^>]*>', '',
notification['status']['content'])
if not self.filter.is_ok(text_content):
continue
self.log('Boosting toot from %s: %s' % (
#notification['status']['id'],
notification['status']['account']['acct'],
notification['status']['content']))
self.m.status_reblog(notification['status']['id'])
retoots.append('%s: %s' % (
notification['status']['account']['acct'],
re.sub(r'@\S*', '', text_content)))
# If the Mastodon instance returns interesting Errors, add them here:
# save state
with os.fdopen(os.open('seen_toots.pickle.part', os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'w') as f:
pickle.dump(self.seen_toots, f)
os.rename('seen_toots.pickle.part', 'seen_toots.pickle')
for mention in self.crawl():
if not trigger.is_ok(mention.text):
continue
self.repost(mention)
retoots.append(mention)
# return mentions for mirroring
return retoots
@ -111,12 +130,28 @@ class RetootBot(object):
if __name__ == '__main__':
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('ticketfrei.cfg') as configfile:
with open('config.toml') as configfile:
config = toml.load(configfile)
filter = trigger.Trigger(config)
bot = RetootBot(config, filter)
fh = logging.FileHandler(config['logging']['logpath'])
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
while True:
bot.retoot()
time.sleep(1)
trigger = trigger.Trigger(config)
bot = RetootBot(config)
try:
while True:
bot.flow(trigger)
time.sleep(1)
except KeyboardInterrupt:
print("Good bye. Remember to restart the bot!")
except:
logger.error('Shutdown', exc_info=True)
try:
mailer = sendmail.Mailer(config)
mailer.send('', config['mail']['contact'],
'Ticketfrei Crash Report',
attachment=config['logging']['logpath'])
except:
logger.error('Mail sending failed', exc_info=True)

247
retweetbot.py Normal file → Executable file
View File

@ -1,13 +1,16 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import twitter
import os
import datetime
import tweepy
import re
import requests
import pytoml as toml
import trigger
from time import sleep
import traceback
import report
import logging
import sendmail
logger = logging.getLogger(__name__)
class RetweetBot(object):
@ -18,47 +21,36 @@ class RetweetBot(object):
api: The api object, generated with your oAuth keys, responsible for
communication with twitter rest API
triggers: a list of words, one of them has to be in a tweet for it to be
retweeted
last_mention: the ID of the last tweet which mentioned you
"""
def __init__(self, trigger, config, historypath="last_mention", logpath=None):
def __init__(self, config, history_path="last_mention"):
"""
Initializes the bot and loads all the necessary data.
:param historypath: Path to the file with ID of the last retweeted
:param config: (dictionary) config.toml as a dictionary of dictionaries
:param history_path: Path to the file with ID of the last retweeted
Tweet
:param logpath: Path to the file where the log is stored
"""
self.config = config
# initialize API access
keys = self.get_api_keys()
self.api = twitter.Api(consumer_key=keys[0],
consumer_secret=keys[1],
access_token_key=keys[2],
access_token_secret=keys[3])
self.historypath = historypath
try:
self.no_shutdown_contact = False
self.user_id = self.config['tapp']['shutdown_contact_userid']
self.screen_name = \
self.config['tapp']['shutdown_contact_screen_name']
except KeyError:
self.no_shutdown_contact = True
self.last_mention = self.get_history(self.historypath)
self.trigger = trigger
auth = tweepy.OAuthHandler(consumer_key=keys[0],
consumer_secret=keys[1])
auth.set_access_token(keys[2], # access_token_key
keys[3]) # access_token_secret
self.api = tweepy.API(auth)
self.history_path = history_path
self.last_mention = self.get_history(self.history_path)
self.waitcounter = 0
if logpath:
self.logpath = logpath
else:
self.logpath = os.path.join("logs", str(datetime.datetime.now()))
print "Path of logfile: " + self.logpath
def get_api_keys(self):
"""
How to get these keys is described in doc/twitter_api.md
After you received keys, store them in your ticketfrei.cfg like this:
After you received keys, store them in your config.toml like this:
[tapp]
consumer_key = "..."
consumer_secret = "..."
@ -69,34 +61,10 @@ class RetweetBot(object):
:return: keys: list of these 4 strings.
"""
keys = []
keys.append(self.config['tapp']['consumer_key'])
keys.append(self.config['tapp']['consumer_secret'])
keys.append(self.config['tuser']['access_token_key'])
keys.append(self.config['tuser']['access_token_secret'])
keys = [self.config['tapp']['consumer_key'], self.config['tapp']['consumer_secret'],
self.config['tuser']['access_token_key'], self.config['tuser']['access_token_secret']]
return keys
def log(self, message, tb=False):
"""
Writing an error message to a logfile in logs/ and prints it.
:param message(string): Log message to be displayed
:param tb: String of the Traceback
"""
time = str(datetime.datetime.now())
if tb:
message = message + " The traceback is located at " + os.path.join("logs" + time)
with open(os.path.join("logs", time), 'w+') as f:
f.write(tb)
line = "[" + time + "] "+ message + "\n"
with open(self.logpath, 'a') as f:
try:
f.write(line)
except UnicodeEncodeError:
self.log("Failed to save log message due to UTF-8 error. ")
traceback.print_exc()
print line,
def get_history(self, path):
""" This counter is needed to keep track of your mentions, so you
don't double RT them
@ -114,9 +82,9 @@ class RetweetBot(object):
f.write(last_mention)
return int(last_mention)
def save_last_mention(self):
def save_last(self):
""" Saves the last retweeted tweet in last_mention. """
with open(self.historypath, "w") as f:
with open(self.history_path, "w") as f:
f.write(str(self.last_mention))
def waiting(self):
@ -130,141 +98,140 @@ class RetweetBot(object):
self.waitcounter -= 1
return self.waitcounter
def format_mastodon(self, status):
"""
Bridge your Retweets to mastodon.
:todo vmann: add all the mastodon API magic.
:param status: Object of a tweet.
:return: toot: text tooted on mastodon, e.g. "_b3yond: There are
uniformed controllers in the U2 at Opernhaus."
"""
toot = status.user.name + ": " + status.text
return toot
def crawl_mentions(self):
def crawl(self):
"""
crawls all Tweets which mention the bot from the twitter rest API.
:return: list of Status objects
:return: reports: (list of report.Report objects)
"""
reports = []
try:
if not self.waiting():
mentions = self.api.GetMentions(since_id=self.last_mention)
return mentions
except twitter.TwitterError:
self.log("Twitter API Error: Rate Limit Exceeded.")
if self.last_mention == 0:
mentions = self.api.mentions_timeline()
else:
mentions = self.api.mentions_timeline(since_id=self.last_mention)
for status in mentions:
text = re.sub("(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)", "", status.text)
reports.append(report.Report(status.author.screen_name,
"twitter",
text,
status.id,
status.created_at))
self.save_last()
return reports
except tweepy.RateLimitError:
logger.error("Twitter API Error: Rate Limit Exceeded", exc_info=True)
self.waitcounter += 60*15 + 1
except requests.exceptions.ConnectionError:
self.log("Twitter API Error: Bad Connection.")
logger.error("Twitter API Error: Bad Connection", exc_info=True)
self.waitcounter += 10
return None
except tweepy.TweepError:
logger.error("Twitter API Error: General Error", exc_info=True)
return []
def retweet(self, status):
def repost(self, status):
"""
Retweets a given tweet.
:param status: A tweet object.
:param status: (report.Report object)
:return: toot: string of the tweet, to toot on mastodon.
"""
while 1:
try:
self.api.PostRetweet(status.id)
self.log("Retweeted: " + self.format_mastodon(status))
self.api.retweet(status.id)
logger.info("Retweeted: " + status.format())
if status.id > self.last_mention:
self.last_mention = status.id
return self.format_mastodon(status)
# maybe one day we get rid of this error. If not, try to uncomment
# these lines.
except twitter.error.TwitterError:
self.log("Twitter API Error: You probably already retweeted this tweet: " + status.text)
if status.id > self.last_mention:
self.last_mention = status.id
return None
self.save_last()
return status.format()
except requests.exceptions.ConnectionError:
self.log("Twitter API Error: Bad Connection.")
logger.error("Twitter API Error: Bad Connection", exc_info=True)
sleep(10)
# maybe one day we get rid of this error:
except tweepy.TweepError:
logger.error("Twitter Error", exc_info=True)
if status.id > self.last_mention:
self.last_mention = status.id
self.save_last()
return None
def tweet(self, post):
def post(self, status):
"""
Tweet a post.
:param post: String with the text to tweet.
:param status: (report.Report object)
"""
if len(post) > 280:
post = post[:280 - 4] + u' ...'
text = status.format()
if len(text) > 280:
text = status.text[:280 - 4] + u' ...'
while 1:
try:
self.api.PostUpdate(status=post)
self.api.update_status(status=text)
return
except requests.exceptions.ConnectionError:
self.log("Twitter API Error: Bad Connection.")
logger.error("Twitter API Error: Bad Connection", exc_info=True)
sleep(10)
def flow(self, to_tweet=()):
def flow(self, trigger, to_tweet=()):
""" The flow of crawling mentions and retweeting them.
:param to_tweet: list of strings to tweet
:return list of retweeted tweets, to toot on mastodon
"""
# Tweet the toots the Retootbot gives to us
# Tweet the reports from other sources
for post in to_tweet:
self.tweet(post)
self.post(post)
# Store all mentions in a list of Status Objects
mentions = self.crawl_mentions()
mastodon = []
mentions = self.crawl()
if mentions is not None:
for status in mentions:
# Is the Text of the Tweet in the triggerlist?
if self.trigger.is_ok(status.text):
# Retweet status
toot = self.retweet(status)
if toot:
mastodon.append(toot)
# initialise list of strings for other bots
all_tweets = []
# save the id so it doesn't get crawled again
if status.id > self.last_mention:
self.last_mention = status.id
self.save_last_mention()
# Return Retweets for tooting on mastodon
return mastodon
for status in mentions:
# Is the Text of the Tweet in the triggerlist?
if trigger.is_ok(status.text):
# Retweet status
toot = self.repost(status)
if toot:
all_tweets.append(toot)
def shutdown(self):
""" If something breaks, it shuts down the bot and messages the owner.
"""
logmessage = "Shit went wrong, closing down."
if self.screen_name:
logmessage = logmessage + " Sending message to " + self.screen_name
self.log(logmessage)
if self.no_shutdown_contact:
return
self.save_last_mention()
try:
self.api.PostDirectMessage("Help! I broke down. restart me pls :$",
self.user_id, self.screen_name)
except:
traceback.print_exc()
print
# Return Retweets for posting on other bots
return all_tweets
if __name__ == "__main__":
# create an Api object
with open('ticketfrei.cfg') as configfile:
# get the config dict of dicts
with open('config.toml') as configfile:
config = toml.load(configfile)
# set log file
fh = logging.FileHandler(config['logging']['logpath'])
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
# initialise trigger
trigger = trigger.Trigger(config)
bot = RetweetBot(trigger, config)
# initialise twitter bot
bot = RetweetBot(config)
try:
while True:
bot.flow()
# :todo separate into small functions
bot.flow(trigger)
sleep(60)
except KeyboardInterrupt:
print "Good bye! Remember to restart the bot."
print("Good bye. Remember to restart the bot!")
except:
traceback.print_exc()
print
bot.shutdown()
logger.error('Shutdown', exc_info=True)
bot.save_last()
try:
mailer = sendmail.Mailer(config)
mailer.send('', config['mail']['contact'],
'Ticketfrei Crash Report',
attachment=config['logging']['logpath'])
except:
logger.error('Mail sending failed', exc_info=True)

75
sendmail.py Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
import smtplib
import ssl
import pytoml as toml
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
class Mailer(object):
"""
Maintains the connection to the mailserver and sends text to users.
"""
def __init__(self, config):
"""
Creates an SMTP client to send a mail. Is called only once
when you actually want to send a mail. After you sent the
mail, the SMTP client is shut down again.
:param config: The config file generated from config.toml
"""
# This generates the From address by stripping the part until the first
# period from the mail server address and won't work always.
self.fromaddr = config["mail"]["user"] + "@" + \
config["mail"]["mailserver"].partition(".")[2]
# starts a client session with the SMTP server
self.s = smtplib.SMTP(config["mail"]["mailserver"])
context = ssl.create_default_context()
self.s.starttls(context=context)
self.s.login(config["mail"]["user"], config["mail"]["passphrase"])
def send(self, text, recipient, subject, attachment=None):
"""
:param text: (string) the content of the mail
:param recipient: (string) the recipient of the mail
:param subject: (string) the subject of the mail
:param attachment: (string) the path to the logfile
:return: string for logging purposes, contains recipient & subject
"""
msg = MIMEMultipart()
msg.attach(MIMEText(text))
msg["From"] = self.fromaddr
msg["To"] = recipient
msg["Subject"] = subject
# attach logfile
if attachment:
with open(attachment, "rb") as fil:
part = MIMEApplication(
fil.read(),
Name="logfile"
)
# After the file is closed
part['Content-Disposition'] = 'attachment; filename="logfile"'
msg.attach(part)
self.s.send_message(msg)
self.s.close()
return "Sent mail to " + recipient + ": " + subject
# For testing:
if __name__ == '__main__':
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('config.toml') as configfile:
config = toml.load(configfile)
m = Mailer(config)
print(m.send("This is a test mail.", m.fromaddr, "Test"))

View File

@ -1,27 +0,0 @@
[mapp]
name = 'yourcity_ticketfrei'
[muser]
email = 'youremail@server.tld'
password = 'yourpassword'
server = 'yourmastodoninstance'
[tapp]
consumer_key = "yourconsumerkey"
consumer_secret = yourconsumersecret"
# shutdown_contact_userid = 012345
# shutdown_contact_screen_name = 'yourscreenname'
[tuser]
access_token_key = "youraccesstokenkey"
access_token_secret = "youraccesstokensecret"
# [trigger]
# goodlists are one regex per line.
# badlists are one badword per line.
# a message musst match at least one regex in goodlist and contain none of the badwords.
# the variables mention the directory where the lists are located, not the filenames.
# goodlist_path = 'goodlists'
# blacklist_path = 'blacklists'

55
ticketfrei.py Normal file → Executable file
View File

@ -1,36 +1,61 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import logging
import pytoml as toml
import time
import traceback
import os
import datetime
import sendmail
from retootbot import RetootBot
from retweetbot import RetweetBot
from mailbot import Mailbot
from trigger import Trigger
if __name__ == '__main__':
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('ticketfrei.cfg') as configfile:
with open('config.toml') as configfile:
config = toml.load(configfile)
# set log file
logger = logging.getLogger()
fh = logging.FileHandler(config['logging']['logpath'])
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
trigger = Trigger(config)
logpath = os.path.join("logs", str(datetime.datetime.now()))
bots = []
mbot = RetootBot(config, trigger, logpath=logpath)
tbot = RetweetBot(trigger, config, logpath=logpath)
if config["muser"]["enabled"] != "false":
bots.append(RetootBot(config))
if config["tuser"]["enabled"] != "false":
bots.append(RetweetBot(config))
if config["mail"]["enabled"] != "false":
bots.append(Mailbot(config))
try:
statuses = []
while True:
statuses = mbot.retoot(statuses)
statuses = tbot.flow(statuses)
time.sleep(60)
for bot in bots:
reports = bot.crawl()
for status in reports:
if not trigger.is_ok(status.text):
continue
for bot2 in bots:
if bot == bot2:
bot2.repost(status)
else:
bot2.post(status)
time.sleep(60) # twitter rate limit >.<
except KeyboardInterrupt:
print "Good bye. Remember to restart the bot!"
print("Good bye. Remember to restart the bot!")
except:
traceback.print_exc()
tbot.shutdown()
logger.error('Shutdown', exc_info=True)
for bot in bots:
bot.save_last()
try:
mailer = sendmail.Mailer(config)
mailer.send('', config['mail']['contact'],
'Ticketfrei Crash Report',
attachment=config['logging']['logpath'])
except:
logger.error('Mail sending failed', exc_info=True)

View File

@ -59,7 +59,7 @@ class Trigger(object):
if __name__ == "__main__":
with open("ticketfrei.cfg", "r") as configfile:
with open("config.toml", "r") as configfile:
config = toml.load(configfile)
print("testing the trigger")