Compare commits

..

67 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 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
89 changed files with 1214 additions and 36195 deletions

View File

@ -1,10 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4
max_line_length = 79

View File

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

View File

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

View File

@ -1,7 +0,0 @@
---
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.*

3
.gitignore vendored
View File

@ -5,16 +5,13 @@ __pycache__/
last_mention
last_mail
ticketfrei.cfg
ticketfrei.sqlite-journal
ticketfrei.sqlite
seen_toots.pickle
seen_toots.pickle.part
pip-selfcheck.json
config.toml
venv/
bin/
include/
lib/
share/
local/
venv/

View File

@ -1,6 +1,5 @@
Copyright (c) 2017 Thomas L <tom@dl6tom.de>
Copyright (c) 2017 b3yond <b3yond@riseup.net>
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

415
README.md
View File

@ -1,379 +1,96 @@
# Ticketfrei social bot
# Ticketfrei micro messaging bot
Ticketfrei is a mastodon/twitter/mail bot to dodge ticket controllers in public
transport systems.
Version: 1.0
## Mission
<!-- This mastodon/twitter bot has one purpose - breaking the law. -->
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.**
The functionality is simple: it retweets every tweet where it is
mentioned.
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.
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.
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!
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 =/
## How It Works
Website: https://wiki.links-tech.org/IT/Ticketfrei
The functionality is simple: It retweets every tweet where it is mentioned.
## Install
This leads to a community which evolves around it. If you see ticket
controllers, tweet their location and mention the bot. The bot then retweets
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.
Setting up a ticketfrei bot for your city is quite easy. Here are the
few steps:
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.
First you need to install python3 and virtualenv with your favourite
package manager.
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.
Create and activate virtualenv:
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!
```shell
sudo apt install python3 virtualenv
virtualenv -p python3 .
. bin/activate
```
Website (our flagship instance): https://ticketfrei.links-tech.org
Install the dependencies:
```shell
pip install tweepy pytoml requests Mastodon.py
```
More information: https://wiki.links-tech.org/IT/Ticketfrei
Configure the bot:
```shell
cp config.toml.example config.toml
vim config.toml
```
## Do you want Ticketfrei in your city?
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.
Just go to https://ticketfrei.links-tech.org or another website where this software is
running.
You have to configure all of the accounts via config.toml; it should
be fairly intuitive to enter the right values.
* 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.
## Maintaining
### Maintaining
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. That way, 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.
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.
### Blocklisting
### blacklisting
You also need to edit the goodlist and the blocklist. You can do this on the
website, in the settings of your bot.
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 ```*```.
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 blocklist, which you can use to automatically sort out
malicious messages. Be careful though, our filter can't read the intention with
which a word was used. Maybe you wanted it there.
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.
## Do you want to offer a Ticketfrei website to others?
### screen
If you want to offer this website to others, feel free to do so. If you have questions, just open
a GitHub issue or write to tech@lists.links-tech.org, we are happy to help and share best practices.
We wrote these installation notes, so you can set up the website easily:
### Install from the git repository
This guide assumes you are on a Debian 9 Server:
To keep the bots running when you are logged out of the shell, you
can use screen:
```shell
sudo apt install python3 virtualenv uwsgi uwsgi-plugin-python3 nginx git exim4
cd /srv
sudo git clone https://github.com/b3yond/ticketfrei
cd ticketfrei
sudo apt-get install screen
echo "if [ -z "$STY" ]; then screen -RR; fi" >> ~/.bash_login
screen
python3 ticketfrei.py
```
Install the necessary packages, create and activate virtualenv:
```shell
virtualenv -p python3 .
. bin/activate
```
Install the dependencies:
```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx gitpython
```
Configure the bot:
```shell
cp config.toml.example config.toml
vim config.toml
```
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
```
Configure exim4 for using mbox files.
```
sudo dpkg-reconfigure exim4-config
# Choose the following values:
# internet site; mail is sent and received directly using SMTP
# your domain name
#
# your domain name
#
#
# No
# mbox format in /var/mail/
# No
```
Deploy ticketfrei with uwsgi:
```shell
echo "Enter your domain name into the following prompt:" && read DOMAIN
# configure nginx
sudo sed -r "s/example.org/$DOMAIN/g" deployment/example.org.conf > /etc/nginx/sites-enabled/$DOMAIN.conf
# create folder for database
sudo mkdir /var/ticketfrei
sudo chown www-data:www-data -R /var/ticketfrei
# create folder for socket
sudo mkdir /var/run/ticketfrei
sudo chown -R www-data:www-data /var/run/ticketfrei
sudo -s
echo "mkdir /var/run/ticketfrei" >> /etc/rc.local
echo "chown -R www-data:www-data /var/run/ticketfrei" >> /etc/rc.local
echo "service ticketfrei-web restart" >> /etc/rc.local
exit
# change /etc/aliases permissions to be able to receive reports per mail
sudo chown root:www-data /etc/aliases
sudo chmod 664 /etc/aliases
# create folder for logs
sudo mkdir /var/log/ticketfrei
sudo chown www-data:www-data -R /var/log/ticketfrei
# start up nginx
sudo service nginx restart
# create and start the frontend systemd service
sudo cp deployment/ticketfrei-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl start ticketfrei-web.service
# create and start the backend systemd service
sudo cp deployment/ticketfrei-backend.service /etc/systemd/system
sudo systemctl daemon-reload
sudo systemctl start ticketfrei-backend.service
```
### Backup
For automated backups, you need to backup these files:
* `/var/ticketfrei/db.sqlite`
* `/srv/ticketfrei/config.toml`
* `/etc/aliases`
You can find an example how to do this with borgbackup in the deployment
folder. Adjust it to your needs.
### Logs
There are several logfiles which you can look at:
```
# for the uwsgi deployment:
less /var/log/ticketfrei/uwsgi.log
# for the backend:
less /var/log/ticketfrei/backend.log
# for the systemd service:
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, 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
sudo git clone https://github.com/b3yond/ticketfrei
cd ticketfrei
git checkout multi-deployment
```
Install the necessary packages, create and activate virtualenv:
```shell
virtualenv -p python3 .
. bin/activate
```
Install the dependencies:
```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx
```
Configure the bot:
```shell
cp config.toml.example config.toml
vim config.toml
```
This configuration is only for the admin. Users can log into
twitter/mastodon/mail and configure their personal bot on the settings page.
```shell
# create folder for socket & database
sudo mkdir /var/ticketfrei
sudo chown $USER:$USER -R /var/ticketfrei
# create folder for logs
sudo mkdir /var/log/ticketfrei
sudo chown $USER:$USER -R /var/log/ticketfrei
# start Ticketfrei
./frontend.py & ./backend.py &
```
# Project History
## Version 1
- more of less hacked together during a mate-fueled weekend
- backend-only, twitter & mastodon
- just a script, which crawled & retweeted tweets, if they match a whitelist & blocklist
- whitelist & blocklist were just 2 files
## Version 2
Reasons for the rewrite:
- user management: Users should be able to run a Ticketfrei bot in their city
- without needing a server, without needing command line skills
- more networks; not only Twitter & Mastodon, also Email & Telegram
2 processes: backend & frontend.
The two Processes talk via a database.
The two Processes have separate log files.
Both processes take some config values from config.toml.
### Backend
The Backend takes care of crawling & spreading the reports.
backend.py:
- main loop which does the crawling & posting.
- loops through all cities in the database
- per city it tries all of the networks/bots:
- per network/bot it runs the crawl()-function to ask the social network for new reports
- then it checks whether the report is appropriate
- if yes, it posts the report via all networks/bots, which belong to the city.
config.py: imports config values
- Imports values from config.toml
- If there is no config file it tries to use environment variables,
- Apart from that it uses the default values.
bot.py: bot parent Class
- just the absolute minimum what a bot needs to be able to do: crawl + post
- is never instantiated, only inherited from
report.py: report Class
- defines how reports are supposed to look like
active_bots/mailbot.py as an example for how a network/bot works
- crawl():
- mails arrive at an mbox file through exim4
- the bot checks whether they are new
- the bot generates a report object from the mail and returns it to the backend.py-loop
- post():
- asks the database for the list of mails which want to receive reports for this city
- sends the report.text to those mail addresses
### Frontend
the architecture of the frontend is loosely oriented off [Model View
Controller](https://blog.codinghorror.com/understanding-model-view-controller/).
user.py (Model)
- high-level interface to talk to the database
- database calls; almost all values in the database are specific to a city/user
- user.py is also a Class for frontend web authentication
- user.py keeps the user-id, through which the frontend tracks authentication
db.py (Model)
- DB-Layout; creates the database if it doesn't exist yet.
- holds some database calls which are not city-specific.
frontend.py (Controller): bottle web application
- handles POST/GET requests
- talks to the database through user.py
- everyone can look at the pages, and register
- but only authenticated users can login and change settings
session.py: User Authentication
- takes care of session cookies and "403 unauthenticated" error messages
sendmail.py: helper script to send mails
- sends all mails the frontend, backend, and bots need to send
static/
- css, images, javascript for the login form etc.
template/ (view)
- base for the HTML generation, uses the bottle-template-framework
- wrapper.tpl is the base template for every other template
### active_bots: how to implement a new network
If you want to write a new bot, e.g. a Wire-Bot, you have to take these steps:
- look for a python-library which can talk to Wire
- the city/users have to provide authentication details; this needs a form in
the settings
- depending on the network either a password, a token, or an implementation
of the OAuth login flow
- the backend needs to crawl messages from the network, & post reports to the
network
Files you need to change:
1. active_bots/wire.py - crawl & post functions
2. settings.tpl - form to authenticate to the network & possible network specific settings.
3. frontend.py - routes for the forms you added to settings.tpl
4. db.py - database layout, to store the account credentials/tokens, and to save which message you have last seen
5. user.py - database calls to get or set values
To log out of the screen session, press "ctrl+a", and then "d".

View File

@ -1,14 +0,0 @@
__all__ = []
import pkgutil
import inspect
for loader, name, is_pkg in pkgutil.walk_packages(__path__):
module = loader.find_module(name).load_module(name)
for name, value in inspect.getmembers(module):
if name.startswith('__'):
continue
globals()[name] = value
__all__.append(name)

View File

@ -1,84 +0,0 @@
#!/usr/bin/env python3
import logging
from sendmail import sendmail
import datetime
import mailbox
import email
import report
from bot import Bot
from config import config
from db import db
logger = logging.getLogger("main")
class Mailbot(Bot):
# returns a list of Report objects
def crawl(self, user):
reports = []
# todo: adjust to actual mailbox
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():
reports.append(make_report(msg, user))
return reports
# post/boost Report object
def post(self, user, report):
recipients = user.get_mailinglist()
for rec in recipients:
rec = rec[0]
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 rec not in report.author:
try:
city = user.get_city()
sendmail(rec, "Ticketfrei " + city + " Report",
city=city, body=body)
except Exception:
logger.error("Sending Mail failed.", exc_info=True)
def make_report(msg, user):
"""
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 = get_date_from_header(msg['Date'])
author = msg['From'] # get mail author from email header
for part in msg.walk():
if part.get_content_type() == 'text/plain':
text = part.get_payload()
elif part.get_content_type() == 'text/html':
text = re.sub(r'<[^>]*>', '', msg.get_payload())
try:
post = report.Report(author, "mail", text, None, date)
except UnboundLocalError:
logger.error('No suitable message body')
return
user.save_seen_mail(date)
return post
def get_date_from_header(header):
"""
:param header: msg['Date']
:return: float: total seconds
"""
date_tuple = email.utils.parsedate_tz(header)
date_tuple = datetime.datetime.fromtimestamp(
email.utils.mktime_tz(date_tuple)
)
return (date_tuple - datetime.datetime(1970, 1, 1)).total_seconds()

View File

@ -1,124 +0,0 @@
#!/usr/bin/env python3
from bot import Bot
import logging
import mastodon
import re
from report import Report
logger = logging.getLogger("main")
class MastodonBot(Bot):
def crawl(self, user):
"""
Crawl mentions from Mastodon.
:return: list of statuses
"""
mentions = []
try:
m = mastodon.Mastodon(*user.get_masto_credentials())
except TypeError:
# No Mastodon Credentials in database.
return mentions
try:
notifications = m.notifications()
except mastodon.MastodonNetworkError:
logger.error("Mastodon Network Error.")
return mentions
except mastodon.MastodonAPIError:
try:
logger.error("Mastodon API Error: " + m.instance()['urls']['streaming_api'] + ", city: " + str(user.uid))
except mastodon.MastodonServerError:
logger.error("Mastodon Server Error 500, can't get instance.")
except mastodon.MastodonVersionError:
logger.error("Mastodon Server Error 500, server version too low.")
return mentions
except mastodon.MastodonInternalServerError:
try:
logger.error("Mastodon Error: 500. Server: " + m.instance()['urls']['streaming_api'])
except mastodon.MastodonServerError:
logger.error("Mastodon Server Error 500, can't get instance.")
except mastodon.MastodonVersionError:
logger.error("Mastodon Server Error 500, server version too low.")
return mentions
except mastodon.MastodonBadGatewayError:
try:
logger.error("Mastodon Error: 502. Server: " + m.instance()['urls']['streaming_api'])
except mastodon.MastodonServerError:
logger.error("Mastodon Server Error 502, can't get instance.")
except mastodon.MastodonVersionError:
logger.error("Mastodon Server Error 502, server version too low.")
return mentions
except mastodon.MastodonServiceUnavailableError:
try:
logger.error("Mastodon Error: 503. Server: " + m.instance()['urls']['streaming_api'])
except mastodon.MastodonServerError:
logger.error("Mastodon Server Error 503, can't get instance.")
except mastodon.MastodonVersionError:
logger.error("Mastodon Server Error 503, server version too low.")
return mentions
except mastodon.MastodonGatewayTimeoutError:
try:
logger.error("Mastodon Error: 504. Server: " + m.instance()['urls']['streaming_api'])
except mastodon.MastodonServerError:
logger.error("Mastodon Server Error 504, can't get instance.")
except mastodon.MastodonVersionError:
logger.error("Mastodon Server Error 504, server version too low.")
return mentions
except mastodon.MastodonServerError:
try:
logger.error("Unknown Mastodon Server Error. Server: " + m.instance()['urls']['streaming_api'], exc_info=True)
except mastodon.MastodonServerError:
logger.error("Unknown Mastodon Server Error.", exc_info=True)
except mastodon.MastodonVersionError:
logger.error("Unknown Mastodon Server Error.", exc_info=True)
return mentions
for status in notifications:
try:
if (status['type'] == 'mention' and
not user.toot_is_seen(status['status']['uri'])):
# save state
user.toot_witness(status['status']['uri'])
# 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)
if status['status']['visibility'] == 'public':
mentions.append(Report(status['account']['acct'],
self,
text,
status['status']['id'],
status['status']['created_at']))
else:
mentions.append(Report(status['account']['acct'],
'mastodonPrivate',
text,
status['status']['id'],
status['status']['created_at']))
except TypeError:
pass
return mentions
def post(self, user, report):
try:
m = mastodon.Mastodon(*user.get_masto_credentials())
except TypeError:
return # no mastodon account for this user.
if report.source == self:
try:
m.status_reblog(report.id)
except Exception:
logger.error('Error boosting: ' + report.id, exc_info=True)
else:
text = report.text
if len(text) > 500:
text = text[:500 - 4] + u' ...'
try:
m.toot(text)
except Exception:
logger.error('Error tooting: ' + user.get_city() + ': ' +
report.id, exc_info=True)

View File

@ -1,92 +0,0 @@
from bot import Bot
import logging
from report import Report
from twx.botapi import TelegramBot as Telegram
logger = logging.getLogger("main")
class TelegramBot(Bot):
def crawl(self, user):
tb = Telegram(user.get_telegram_credentials())
seen_tg = user.get_seen_tg()
try:
updates = tb.get_updates(offset=seen_tg + 1,
allowed_updates="message",
timeout=5).wait()
except TypeError:
updates = tb.get_updates(timeout=5).wait()
reports = []
if updates == None:
return reports
for update in updates:
# return when telegram returns an error code
if update in [303, 404, 420, 500, 502]:
return reports
# log unusual telegram error messages
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
# save the last message, so it doesn't get crawled again
user.save_seen_tg(update.update_id)
# skip if message is None
if update.message is None:
continue
# complain if message is a photo
if update.message.photo is not None:
tb.send_message(
update.message.sender.id,
"Sending Photos is not supported for privacy reasons. Can "
"you describe it as text instead?")
continue
# complain if message is a media file
if update.message.text is None:
tb.send_message(
update.message.sender.id,
"We only support text reporting 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(
update.message.sender.id,
"You are now subscribed to report notifications.")
# TODO: /start message should be set in frontend
elif update.message.text.lower() == "/stop":
user.remove_telegram_subscribers(update.message.sender.id)
tb.send_message(
update.message.sender.id,
"You are now unsubscribed from report notifications.")
# TODO: /stop message should be set in frontend
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.")
# TODO: /help message should be set in frontend
else:
# 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 - 2] + " \N{Horizontal ellipsis}"
try:
for subscriber_id in user.get_telegram_subscribers():
tb.send_message(subscriber_id, text).wait()
except Exception:
logger.error('Error telegramming: ' + user.get_city() + ': '
+ str(report.id), exc_info=True)

View File

@ -1,90 +0,0 @@
#!/usr/bin/env python3
import logging
import tweepy
import re
import requests
from time import time
import report
from bot import Bot
logger = logging.getLogger("main")
class TwitterBot(Bot):
def get_api(self, user):
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
keys[3]) # access_token_secret
return tweepy.API(auth, wait_on_rate_limit=True)
def crawl(self, user):
"""
crawls all Tweets which mention the bot from the twitter rest API.
:return: reports: (list of report.Report objects)
"""
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:
# When there is no twitter account for this bot, we want to
# seamlessly continue.
#logger.error("Error Authenticating Twitter", exc_info=True)
return reports
last_mention = user.get_seen_tweet()
try:
if last_mention == 0:
mentions = api.mentions_timeline()
else:
mentions = api.mentions_timeline(since_id=last_mention)
user.set_last_twitter_request(time())
for status in mentions:
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,
status.id,
status.created_at))
user.save_seen_tweet(status.id)
return reports
except tweepy.RateLimitError:
logger.error("Twitter API Error: Rate Limit Exceeded",
exc_info=True)
# :todo implement rate limiting
except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection", exc_info=True)
except tweepy.TweepError:
logger.error("Twitter API Error: General Error. User: " + str(user.uid), exc_info=True)
return []
def post(self, user, report):
try:
api = self.get_api(user)
except TypeError:
return # no twitter account for this user.
try:
if report.source == self:
api.retweet(report.id)
else:
text = report.text
if len(text) > 280:
text = text[:280 - 4] + u' ...'
api.update_status(status=text)
except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection",
exc_info=True)
except tweepy.error.TweepError:
logger.error("Twitter API Error", exc_info=True)

2
appkeys/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -1,49 +0,0 @@
#!/usr/bin/env python3
from bot import Bot
import active_bots
from config import config
from db import db
import logging
from sendmail import sendmail
from time import sleep
def shutdown():
try:
sendmail(config['web']['contact'], 'Ticketfrei Shutdown')
except Exception:
logger.error('Could not inform admin.', exc_info=True)
exit(1)
if __name__ == '__main__':
logger = logging.getLogger("main")
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler('/var/log/ticketfrei/backend.log')
formatter = logging.Formatter('%(asctime)s %(levelname)8s: %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.info("Backend Daemon was started...")
bots = []
for ActiveBot in active_bots.__dict__.values():
if isinstance(ActiveBot, type) and issubclass(ActiveBot, Bot):
bots.append(ActiveBot())
try:
while True:
for user in db.active_users:
for bot in bots:
sleep(1)
reports = bot.crawl(user)
for status in reports:
if not user.is_appropriate(status):
logger.info("Inaproppriate message: %d %s %s" % (user.uid, status.author, status.text))
continue
for bot2 in bots:
sleep(1)
bot2.post(user, status)
logger.info("Resent: %d %s %s" % (user.uid, status.author, status.text))
except Exception:
logger.error("Shutdown.", exc_info=True)
shutdown()

13
blacklists/nbg_blacklist Normal file
View File

@ -0,0 +1,13 @@
bastard
bitch
whore
hitler
slut
hure
jude
schwuchtel
fag
faggot
nigger
neger
schlitz

9
bot.py
View File

@ -1,9 +0,0 @@
class Bot(object):
# returns a list of Report objects
def crawl(self, user):
reports = []
return reports
# post/boost Report object
def post(self, user, report):
pass

View File

@ -1,70 +0,0 @@
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)
try:
with open('config.toml') as configfile:
config = toml.load(configfile)
except FileNotFoundError:
config = load_env()
if __name__ == "__main__":
for category in config:
for key in config[category]:
print(key + "=" + str(config[category][key]))

View File

@ -1,16 +1,48 @@
[twitter]
[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"
[web]
host = "0.0.0.0" # will be used by bottle as a host.
port = 80
contact = "b3yond@riseup.net"
[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]
mbox_user = "root"
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"
[database]
db_path = "/var/ticketfrei/db.sqlite"
[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'

319
db.py
View File

@ -1,319 +0,0 @@
from config import config
import jwt
import logging
from os import urandom, system
from pylibscrypt import scrypt_mcf
import sqlite3
from time import sleep, time
logger = logging.getLogger("main")
class DB(object):
def __init__(self, dbfile):
self.conn = sqlite3.connect(dbfile)
self.cur = self.conn.cursor()
self.create()
def execute(self, *args, **kwargs):
return self.cur.execute(*args, **kwargs)
def commit(self):
start_time = time()
while 1:
try:
self.conn.commit()
break
except sqlite3.OperationalError as error:
# another thread may be writing, give it a chance to finish
sleep(0.5)
logger.exception()
if time() - start_time > 5:
# if it takes this long, something is wrong
system("rcctl restart frontend_daemon")
logger.warning("frontend_daemon is getting restarted")
self.conn.commit()
def close(self):
self.conn.close()
def create(self):
# init db
self.cur.executescript('''
CREATE TABLE IF NOT EXISTS user (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
passhash TEXT,
enabled INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS email (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
email TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS triggerpatterns (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
patterns TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS badwords (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
words TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS mastodon_instances (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
instance TEXT,
client_id TEXT,
client_secret TEXT
);
CREATE TABLE IF NOT EXISTS mastodon_accounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
access_token TEXT,
instance_id INTEGER,
active INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id),
FOREIGN KEY(instance_id) REFERENCES mastodon_instances(id)
);
CREATE TABLE IF NOT EXISTS seen_toots (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
toot_uri TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS seen_telegrams (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
tg_id INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS twitter_request_tokens (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
request_token TEXT,
request_token_secret TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS twitter_accounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
client_id TEXT,
client_secret TEXT,
active INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS telegram_accounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
apikey TEXT,
active INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS seen_tweets (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
tweet_id INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS seen_dms (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
twitter_accounts_id INTEGER,
message_id TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
FOREIGN KEY(twitter_accounts_id)
REFERENCES twitter_accounts(id)
);
CREATE TABLE IF NOT EXISTS telegram_subscribers (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
subscriber_id INTEGER,
FOREIGN KEY(user_id) REFERENCES user(id),
UNIQUE(user_id, subscriber_id) ON CONFLICT IGNORE
);
CREATE TABLE IF NOT EXISTS mailinglist (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
email TEXT,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE IF NOT EXISTS seen_mail (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
user_id INTEGER,
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,
city TEXT,
markdown TEXT,
mail_md TEXT,
masto_link TEXT,
twit_link TEXT,
FOREIGN KEY(user_id) REFERENCES user(id),
UNIQUE(user_id, city) ON CONFLICT IGNORE
);
CREATE TABLE IF NOT EXISTS secret (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
secret BLOB
);
''')
def get_secret(self):
"""
At __init__(), the db needs a secret. It tries to fetch it from the db,
and if it fails, it generates a new one.
:return:
"""
# select only the newest secret. should be only one row anyway.
self.execute("SELECT secret FROM secret ORDER BY id DESC LIMIT 1")
try:
return self.cur.fetchone()[0]
except TypeError:
new_secret = urandom(32)
self.execute("INSERT INTO secret (secret) VALUES (?);",
(new_secret, ))
self.commit()
return new_secret
def user_token(self, email, password):
"""
This function is called by the register confirmation process. It wants
to write an email to the email table and a passhash to the user table.
:param email: a string with an E-Mail address.
:param password: a string with a passhash.
:return:
"""
return jwt.encode({
'email': email,
'passhash': scrypt_mcf(
password.encode('utf-8')
).decode('ascii')
}, self.get_secret()).decode('ascii')
def mail_subscription_token(self, email, city):
"""
This function is called by the mail subscription process. It wants
to write an email to the mailinglist table.
:param email: string
:param city: string
:return: a token with an encoded json dict { email: x, city: y }
"""
token = jwt.encode({
'email': email,
'city': city
}, self.get_secret()).decode('ascii')
return token
def confirm_subscription(self, token):
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.get_secret())
except jwt.DecodeError:
return None # invalid token
if 'passhash' in json.keys():
# create user
self.execute("INSERT INTO user (passhash) VALUES(?);",
(json['passhash'], ))
uid = self.cur.lastrowid
default_triggerpatterns = """kontroll?e
konti
db
vgn
vag
zivil
sicherheit
uniform
station
bus
bahn
tram
linie
nuernberg
nürnberg
s\d
u\d\d?"""
self.execute("""INSERT INTO triggerpatterns (user_id, patterns)
VALUES(?, ?); """, (uid, default_triggerpatterns))
self.execute("INSERT INTO badwords (user_id, words) VALUES(?, ?);",
(uid, "bastard"))
else:
uid = json['uid']
with open("/etc/aliases", "a+") as f:
f.write(city + ": " + config["mail"]["mbox_user"] + "\n")
try:
os.system("newaliases")
except:
logger.exception()
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 twitter_last_request (user_id, date) VALUES (?, ?)", (uid, 0))
self.commit()
user = User(uid)
user.set_city(city)
return user
def by_email(self, email):
from user import User
self.execute("SELECT user_id FROM email WHERE email=?;", (email, ))
try:
uid, = self.cur.fetchone()
except TypeError:
return None
return User(uid)
def by_city(self, city):
from user import User
self.execute("SELECT user_id FROM cities WHERE city=?", (city, ))
try:
uid, = self.cur.fetchone()
except TypeError:
return None
return User(uid)
def user_facing_properties(self, city):
self.execute("""SELECT city, markdown, mail_md, masto_link, twit_link
FROM cities
WHERE city=?;""", (city, ))
try:
city, markdown, mail_md, masto_link, twit_link = self.cur.fetchone()
return dict(city=city,
markdown=markdown,
mail_md=mail_md,
masto_link=masto_link,
twit_link=twit_link,
mailinglist=city + "@" + config["web"]["host"])
except TypeError:
return None
@property
def active_users(self):
from user import User
self.execute("SELECT id FROM user WHERE enabled=1;")
return [User(uid) for uid, in self.cur.fetchall()]
db = DB(config['database']['db_path'])

View File

@ -1,15 +0,0 @@
#!/bin/ksh
daemon="/usr/local/bin/python3 /srv/ticketfrei/backend.py"
daemon_user="root"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_start() {
rc_exec "cd /srv/ticketfrei; /usr/bin/nice -n15 ${daemon} ${daemon_flags}"
}
rc_cmd $1

View File

@ -1,15 +0,0 @@
#!/bin/ksh
. /etc/borg-env
export BORG_REPO=nathan@nephilim:repositories-borg/ticketfrei
export BORG_RSH="ssh \
-o TCPKeepAlive=no \
-o ServerAliveInterval=15 \
-o ServerAliveCountMax=10 \
-o Compression=no"
rcctl stop backend_daemon
rcctl stop frontend_daemon
/usr/local/bin/borg create --stats ::'backup{now:%Y%m%d-%H%M}' /srv/ticketfrei /var/ticketfrei /etc
rcctl start backend_daemon
rcctl start frontend_daemon

View File

@ -1,15 +0,0 @@
#!/bin/ksh
daemon="/usr/local/bin/python3 /srv/ticketfrei/frontend.py"
daemon_user="frontend"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_start() {
rc_exec "env > /tmp/envars; cd /srv/ticketfrei; ${daemon} ${daemon_flags}"
}
rc_cmd $1

View File

@ -1,11 +0,0 @@
[uwsgi]
plugins = python3
master = true
uid = www-data
gid = www-data
processes = 1
logto = /var/log/ticketfrei/uwsgi.log
socket = /var/run/ticketfrei/ticketfrei.sock
chmod-socket = 660
wsgi-file = /srv/ticketfrei/frontend.py
virtualenv = /srv/ticketfrei

View File

@ -1,277 +0,0 @@
#!/usr/bin/env python3
import bottle
from bottle import get, post, redirect, request, response, view
from config import config
from db import db
import logging
import tweepy
from sendmail import sendmail
from session import SessionPlugin
from mastodon import Mastodon
def url(route):
return '%s://%s/%s' % (
request.urlparts.scheme,
request.urlparts.netloc,
route)
@get('/')
@view('template/propaganda.tpl')
def propaganda():
pass
@post('/register')
@view('template/register.tpl')
def register_post():
try:
email = request.forms['email']
password = request.forms['pass']
password_repeat = request.forms['pass-repeat']
city = request.forms['city']
except KeyError:
return dict(error='Please, fill the form.')
if password != password_repeat:
return dict(error='Passwords do not match.')
if db.by_email(email):
return dict(error='Email address already in use.')
# send confirmation mail
try:
link = url('confirm/' + city + '/%s' % db.user_token(email, password))
print(link) # only for local testing
logger.error('confirmation link to ' + email + ": " + link)
sendmail(
email,
"Confirm your account",
body="Complete your registration here: %s" % (link)
)
return dict(info='Confirmation mail sent.')
except Exception:
logger.error("Could not send confirmation mail to " + email, exc_info=True)
return dict(error='Could not send confirmation mail.')
@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='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')
@view('template/login.tpl')
def login_post():
# check login
try:
if db.by_email(request.forms['email']) \
.check_password(request.forms['pass']):
redirect('/settings')
except KeyError:
return dict(error='Please, fill the form.')
except AttributeError:
pass
return dict(error='Authentication failed.')
@get('/city/<city>')
def city_page(city, info=None):
citydict = db.user_facing_properties(city)
if citydict is not None:
citydict['info'] = info
return bottle.template('template/city.tpl', **citydict)
return bottle.template('template/propaganda.tpl',
**dict(info='There is no Ticketfrei bot in your city'
' yet. Create one yourself!'))
@get('/city/mail/<city>')
@view('template/mail.tpl')
def display_mail_page(city):
user = db.by_city(city)
return user.state()
@post('/city/mail/submit/<city>')
def subscribe_mail(city):
email = request.forms['mailaddress']
token = db.mail_subscription_token(email, city)
confirm_link = url('city/mail/confirm/' + token)
print(confirm_link) # only for local testing
# 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=city)
return city_page(city, info="Thanks! You will receive a confirmation mail.")
@get('/city/mail/confirm/<token>')
def confirm_subscribe(token):
email, city = db.confirm_subscription(token)
user = db.by_city(city)
user.add_subscriber(email)
return city_page(city, info="Thanks for subscribing to mail notifications!")
@get('/city/mail/unsubscribe/<token>')
def unsubscribe(token):
email, city = db.confirm_subscription(token)
user = db.by_city(city)
user.remove_subscriber(email)
return city_page(city, info="You successfully unsubscribed " + email +
" from the mail notifications.")
@get('/settings')
@view('template/settings.tpl')
def settings(user):
return user.state()
@post('/settings/markdown')
@view('template/settings.tpl')
def update_markdown(user):
user.set_markdown(request.forms['markdown'])
return user.state()
@post('/settings/mail_md')
@view('template/settings.tpl')
def update_mail_md(user):
user.set_mail_md(request.forms['mail_md'])
return user.state()
@post('/settings/goodlist')
@view('template/settings.tpl')
def update_trigger_patterns(user):
user.set_trigger_words(request.forms['goodlist'])
return user.state()
@post('/settings/blocklist')
@view('template/settings.tpl')
def update_badwords(user):
user.set_badwords(request.forms['blocklist'])
return user.state()
@post('/settings/telegram')
def register_telegram(user):
apikey = request.forms['apikey']
user.update_telegram_key(apikey)
return city_page(user.get_city(), info="Thanks for registering Telegram!")
# unused afaik
#@get('/api/state')
#def api_enable(user):
# return user.state()
@get('/static/<filename:path>')
def static(filename):
return bottle.static_file(filename, root='static')
@get('/guides/<filename:path>')
def guides(filename):
return bottle.static_file(filename, root='guides')
@get('/logout/')
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('/')
@get('/login/twitter')
def login_twitter(user):
"""
Starts the twitter OAuth authentication process.
:return: redirect to twitter.
"""
consumer_key = config["twitter"]["consumer_key"]
consumer_secret = config["twitter"]["consumer_secret"]
callback_url = url("login/twitter/callback")
auth = tweepy.OAuthHandler(consumer_key, consumer_secret, callback_url)
try:
redirect_url = auth.get_authorization_url()
except tweepy.TweepError:
logger.error('Twitter OAuth Error: Failed to get request token.',
exc_info=True)
return dict(error="Failed to get request token.")
user.save_request_token(auth.request_token)
redirect(redirect_url)
@get('/login/twitter/callback')
def twitter_callback(user):
"""
Gets the callback
:return:
"""
# twitter passes the verifier/oauth token secret in a GET request.
verifier = request.query['oauth_verifier']
consumer_key = config["twitter"]["consumer_key"]
consumer_secret = config["twitter"]["consumer_secret"]
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
request_token = user.get_request_token()
auth.request_token = request_token
auth.get_access_token(verifier)
user.save_twitter_token(auth.access_token, auth.access_token_secret)
redirect("/settings")
@post('/login/mastodon')
def login_mastodon(user):
"""
Mastodon OAuth authentication process.
:return: redirect to city page.
"""
# get app tokens
instance_url = request.forms.get('instance_url')
masto_email = request.forms.get('email')
masto_pass = request.forms.get('pass')
client_id, client_secret = user.get_mastodon_app_keys(instance_url)
m = Mastodon(client_id=client_id, client_secret=client_secret,
api_base_url=instance_url)
try:
access_token = m.log_in(masto_email, masto_pass)
user.save_masto_token(access_token, 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)
return dict(error='Login to Mastodon failed.')
logger = logging.getLogger()
fh = logging.FileHandler('/var/log/ticketfrei/error.log')
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
application = bottle.default_app()
bottle.install(SessionPlugin('/'))
if __name__ == '__main__':
bottle.run(host="0.0.0.0", port=config["web"]["port"])
else:
application.catchall = False

17
goodlists/nbg_goodlist Normal file
View File

@ -0,0 +1,17 @@
kontroll?e
konti
db
vgn
vag
zivil
sicherheit
uniform
station
bus
bahn
tram
linie
nuernberg
nürnberg
s\d
u\d\d?

BIN
guides/toot_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

43
guides/use.md Normal file
View File

@ -0,0 +1,43 @@
# How to use Ticketfrei
Do you want to help sousveilling ticket controllers?
## Do you want to know if it's safe to ride without a ticket at the moment?
Just look at the profile of the bot: https://chaos.social/@nbg_ticketfrei
Do you see a toot, reporting ticket controllers?
* If yes, you should probably buy a ticket for now.
In Nuremberg we made the experience that ticket controllers are usually active for about a week, a few hours every day.
So if you see that there was a warning in the last days, watch out.
* If no, you are probably fine! Dare to ride without a ticket.
We can't guarantee that you will be safe though, so still watch out.
The more people participate, the more you can trust that controllers are reported before you run into them.
So, if you have bad luck and are the first one to see the controller:
## Do you want to help others, who ride public transport without a ticket?
That's easy. You only need an Mastodon account, for example at
* https://queer.party/about
* https://soc.ialis.me/about
* https://witches.town/about
* https://kitty.town/about
* https://social.coop/about
* https://awoo.space/about
* or a Twitter account.
Just write a toot or a tweet, mentioning the bot, and tell it
* Where you saw the ticket controllers
* Which line they are using, into which directions
For example like this:
![Screenshot of tooting](tooting_screenshot.png)
![A toot ready to be boosted](toot_screenshot.png)
The bot will soon boost your toot, so other people will be able to look at it and be safe.
Thanks for helping to provide public transport for everyone!

4
logs/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

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?

18
promotion/flyer.md Normal file
View File

@ -0,0 +1,18 @@
Flyer, der bestimmt gut an Universitäten geht:
# Ticketfrei fahren? Nur wenn wir zusammenhelfen!
Das Problem sind immer die Kontrolleure.
Ein Kontrolleur hat zwei Augen, zwei zuviel.
Wenn man nur wüsste, wo gerade welche sind - und wo nicht.
Hundertausende Menschen fahren täglich U-Bahn.
Hunderttausende Augen, die wissen, wo gerade Kontrolleure sind.
Sei solidarisch! Twittere an @nbg_ticketfrei, wenn du sie bei ihrer Arbeit siehst.
Überwache die Überwacher.
Ticketfrei - Wir sind unser eigenes Solidarticket.
Lies hier, wie du mitmachen kannst: [qr-code.png]
gez. Netzwerk für kybernetischen Anarchismus & Sousveillance

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

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3
class Report(object):
"""
A ticketfrei report object.
@ -14,13 +13,24 @@ class Report(object):
Constructor of a ticketfrei report
:param author: username of the author
:param source: mastodon, twitter, or email bot object
: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.source = source
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

View File

@ -1,9 +0,0 @@
tweepy
pytoml
Mastodon.py
bottle
pyjwt
pylibscrypt
Markdown
twx
gitpython

157
retootbot.py Executable file
View File

@ -0,0 +1,157 @@
#!/usr/bin/env python3
import pytoml as toml
import mastodon
import os
import pickle
import re
import time
import trigger
import logging
import sendmail
import report
logger = logging.getLogger(__name__)
class RetootBot(object):
def __init__(self, config):
self.config = config
self.client_id = self.register()
self.m = self.login()
# load state
try:
with open('seen_toots.pickle', 'rb') as f:
self.seen_toots = pickle.load(f)
except IOError:
self.seen_toots = set()
def register(self):
client_id = os.path.join(
'appkeys',
self.config['mapp']['name'] +
'@' + self.config['muser']['server']
)
if not os.path.isfile(client_id):
mastodon.Mastodon.create_app(
self.config['mapp']['name'],
api_base_url=self.config['muser']['server'],
to_file=client_id
)
return client_id
def login(self):
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 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 report in reports:
self.post(report)
# boost mentions
retoots = []
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
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)
fh = logging.FileHandler(config['logging']['logpath'])
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
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)

237
retweetbot.py Executable file
View File

@ -0,0 +1,237 @@
#!/usr/bin/env python3
import tweepy
import re
import requests
import pytoml as toml
import trigger
from time import sleep
import report
import logging
import sendmail
logger = logging.getLogger(__name__)
class RetweetBot(object):
"""
This bot retweets all tweets which
1) mention him,
2) contain at least one of the triggerwords provided.
api: The api object, generated with your oAuth keys, responsible for
communication with twitter rest API
last_mention: the ID of the last tweet which mentioned you
"""
def __init__(self, config, history_path="last_mention"):
"""
Initializes the bot and loads all the necessary data.
:param config: (dictionary) config.toml as a dictionary of dictionaries
:param history_path: Path to the file with ID of the last retweeted
Tweet
"""
self.config = config
# initialize API access
keys = self.get_api_keys()
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
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 config.toml like this:
[tapp]
consumer_key = "..."
consumer_secret = "..."
[tuser]
access_token_key = "..."
access_token_secret = "..."
:return: keys: list of these 4 strings.
"""
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 get_history(self, path):
""" This counter is needed to keep track of your mentions, so you
don't double RT them
:param path: string: contains path to the file where the ID of the
last_mention is stored.
:return: last_mention: ID of the last tweet which mentioned the bot
"""
try:
with open(path, "r+") as f:
last_mention = f.read()
except IOError:
with open(path, "w+") as f:
last_mention = "0"
f.write(last_mention)
return int(last_mention)
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_mention))
def waiting(self):
"""
If the counter is not 0, you should be waiting instead.
:return: self.waitcounter(int): if 0, do smth.
"""
if self.waitcounter > 0:
sleep(1)
self.waitcounter -= 1
return self.waitcounter
def crawl(self):
"""
crawls all Tweets which mention the bot from the twitter rest API.
:return: reports: (list of report.Report objects)
"""
reports = []
try:
if not self.waiting():
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:
logger.error("Twitter API Error: Bad Connection", exc_info=True)
self.waitcounter += 10
except tweepy.TweepError:
logger.error("Twitter API Error: General Error", exc_info=True)
return []
def repost(self, status):
"""
Retweets a given tweet.
:param status: (report.Report object)
:return: toot: string of the tweet, to toot on mastodon.
"""
while 1:
try:
self.api.retweet(status.id)
logger.info("Retweeted: " + status.format())
if status.id > self.last_mention:
self.last_mention = status.id
self.save_last()
return status.format()
except requests.exceptions.ConnectionError:
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 post(self, status):
"""
Tweet a post.
:param status: (report.Report object)
"""
text = status.format()
if len(text) > 280:
text = status.text[:280 - 4] + u' ...'
while 1:
try:
self.api.update_status(status=text)
return
except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection", exc_info=True)
sleep(10)
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 reports from other sources
for post in to_tweet:
self.post(post)
# Store all mentions in a list of Status Objects
mentions = self.crawl()
# initialise list of strings for other bots
all_tweets = []
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)
# Return Retweets for posting on other bots
return all_tweets
if __name__ == "__main__":
# 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)
# initialise twitter bot
bot = RetweetBot(config)
try:
while True:
# :todo separate into small functions
bot.flow(trigger)
sleep(60)
except KeyboardInterrupt:
print("Good bye. Remember to restart the bot!")
except:
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)

View File

@ -1,31 +1,75 @@
#!/usr/bin/env python3
from config import config
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
from getpass import getuser
import smtplib
from socket import getfqdn
import ssl
import pytoml as toml
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
logger = logging.getLogger("main")
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.
def sendmail(to, subject, city=None, body=''):
msg = MIMEMultipart()
if city:
msg['From'] = 'Ticketfrei <%s@%s>' % (city, getfqdn())
else:
msg['From'] = 'Ticketfrei <%s@%s>' % (getuser(), getfqdn())
msg['To'] = to
msg['Subject'] = '[Ticketfrei] %s' % (subject, )
msg.attach(MIMEText(body))
: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]
with smtplib.SMTP('localhost') as smtp:
smtp.send_message(msg)
# 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__':
sendmail(config['web']['contact'], "Test Mail",
body="This is a test mail.")
# 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,32 +0,0 @@
from bottle import redirect, request, abort, response
from db import db
from functools import wraps
from inspect import Signature
from user import User
class SessionPlugin(object):
name = 'SessionPlugin'
keyword = 'user'
api = 2
def __init__(self, loginpage):
self.loginpage = loginpage
def apply(self, callback, route):
if self.keyword in Signature.from_callable(route.callback).parameters:
@wraps(callback)
def wrapper(*args, **kwargs):
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
else:
return callback

View File

@ -1,106 +0,0 @@
<head>
<title>Ticketfrei</title>
<link rel='stylesheet' href='/static/css/style.css'>
<meta name='og:title' content='Settings - Ticketfrei'/>
<meta name='og:description' content='A bot against control society! Nobody should have to pay for public transport. Find out where ticket controllers are!'/>
<meta name='og:image' content="https://ticketfrei.links-tech.org/static/img/ticketfrei-og-image.png"/>
<meta name='og:image:alt' content='Ticketfrei'/>
<meta name='og:type' content='website' />
</head>
<body>
<div class="area">
<h1><a href="/"><img src="/static/img/ticketfrei_logo.png" alt="Ticketfrei" height="150px" align="center" style="float: none;"></a></h1>
<div id="enablebutton" style="float: right; padding: 2em;">asdf</div>
<a class='button' style="padding: 1.5em;" href="/login/twitter">
<picture>
<source type='image/webp' sizes='20px' srcset="/static-cb/1517673283/twitter-20.webp 20w,/static-cb/1517673283/twitter-40.webp 40w,/static-cb/1517673283/twitter-80.webp 80w,"/>
<source type='image/png' sizes='20px' srcset="/static-cb/1517673283/twitter-20.png 20w,/static-cb/1517673283/twitter-40.png 40w,/static-cb/1517673283/twitter-80.png 80w,"/>
<img src="https://codl.forget.fr/static-cb/1517673283/twitter-20.png" alt="" />
</picture>
Log in with Twitter
</a>
<section style="padding: 1.5em;">
<h2>Log in with Mastodon</h2>
<p>
<form action="/login/mastodon" method='post'>
<label>Mastodon instance:
<input type='text' name='instance_url' list='instances' placeholder='social.example.net'/>
</label>
<datalist id='instances'>
<option value=''>
<option value='anticapitalist.party'>
<option value='awoo.space'>
<option value='cybre.space'>
<option value='mastodon.social'>
<option value='glitch.social'>
<option value='botsin.space'>
<option value='witches.town'>
<option value='social.wxcafe.net'>
<option value='monsterpit.net'>
<option value='mastodon.xyz'>
<option value='a.weirder.earth'>
<option value='chitter.xyz'>
<option value='sins.center'>
<option value='dev.glitch.social'>
<option value='computerfairi.es'>
<option value='niu.moe'>
<option value='icosahedron.website'>
<option value='hostux.social'>
<option value='hyenas.space'>
<option value='instance.business'>
<option value='mastodon.sdf.org'>
<option value='pawoo.net'>
<option value='pouet.it'>
<option value='scalie.business'>
<option value='sleeping.town'>
<option value='social.koyu.space'>
<option value='sunshinegardens.org'>
<option value='vcity.network'>
<option value='octodon.social'>
<option value='soc.ialis.me'>
</datalist>
<input name='confirm' value='Log in' type='submit'/>
</form>
</p>
</section>
<!-- offer mailing list creation button -->
<div style="float: left; padding: 1.5em;">
<!-- good list entry field -->
<p>
These words have to be contained in a report.
If none of these expressions is in the report, it will be ignored by the bot.
You can use the defaults, or enter some expressions specific to your city and language.
</p>
<form action="/settings/goodlist" method="post">
<!-- find a way to display current good list. js which reads from a cookie? template? -->
<textarea id="goodlist" rows="8" cols="70" name="goodlist" wrap="physical"></textarea>
<input name='confirm' value='Submit' type='submit'/>
</form>
</div>
<!-- blocklist entry field -->
<div style="float:right; padding: 1.5em;">
<p>
These words are not allowed in reports.
If you encounter spam, you can add more here - the bot will ignore reports which use such words.
<!-- There are words which you can't exclude from the blocklist, e.g. certain racist, sexist, or antisemitic slurs. (to be implemented) -->
</p>
<form action="/settings/blocklist" method="post">
<!-- find a way to display current blocklist. js which reads from a cookie? template? -->
<textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical"></textarea>
<input name='confirm' value='Submit' type='submit'/>
</form>
</div>
<script src="/static/js/functions.js"></script>
<div class=footer>
Contribute on <a href="https://github.com/b3yond/ticketfrei">GitHub!</a>
</div>
</div>
</body>

View File

@ -1,47 +0,0 @@
body {
background-image: url(/static/img/ticketfrei-og-image.jpg);
background-height: 100%;
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 12pt;
line-height: 1.5em;
background-position: center top;
margin: 0;
}
#logo {
height: 9em;
}
#content {
background-color: #FFF;
max-width: 37em;
margin-left: auto;
margin-right: auto;
padding: 2em;
}
button {
background-color: #4CAF50;
color: white;
padding: 0.7em 1em;
margin: 0.5em 0;
border: none;
cursor: pointer;
font-size: 120%;
}
button:hover {
opacity: 0.8;
}
input[type=text], input[type=password] {
width: 100%;
padding: 0.8em 1em;
margin: 0.5em 0;
display: inline-block;
border: 1px solid #ccc;
}
h2 {
padding-top: 1em;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,82 +0,0 @@
<head>
<title>Ticketfrei</title>
<link rel='stylesheet' href='/static/css/style.css'>
<meta name='og:title' content='Ticketfrei'/>
<meta name='og:description' content='A bot against control society! Nobody should have to pay for public transport. Find out where ticket controllers are!'/>
<meta name='og:image' content="https://ticketfrei.links-tech.org/static/img/ticketfrei-og-image.png"/>
<meta name='og:image:alt' content='Ticketfrei'/>
<meta name='og:type' content='website' />
</head>
<body>
<div class="area">
<h1><a href="/"><img src="/static/img/ticketfrei_logo.png" alt="Ticketfrei" height="150px" align="center" style="float: none;"></a></h1>
<form action="/login" method="POST">
<div class="container">
<label><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="uname" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="psw" required>
<span style="float:center">
<button type="submit">Login</button>
</span>
<br>
<span class="psw" style="float: right;">
Forgot <a href="#">password?</a>
</span>
</div>
</form>
<div class=text>
<h1>Features</h1>
<p>sum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard
dummy text ever since the 1500s, when an unknown printer
took a galley of type and scrambled it to make a type
specimen book. It has survived not only five centuries,
but also the leap into electronic typesetting, remaining
essentially unchanged. It was popularised in the 1960s
with the release of Letraset sheets containing Lorem
Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of Lorem
Ipsum.</p>
<h2>How to get Ticketfrei to my city?</h2>
<p>sum is simply dummy text of the printing and typesetting
industry. Lorem Ipsum has been the industry's standard
dummy text ever since the 1500s, when an unknown printer
took a galley of type and scrambled it to make a type
specimen book. It has survived not only five centuries,
but also the leap into electronic typesetting, remaining
essentially unchanged. It was popularised in the 1960s
with the release of Letraset sheets containing Lorem
Ipsum passages, and more recently with desktop publishing
software like Aldus PageMaker including versions of Lorem
Ipsum.</p>
<a href="static/register.html"><button>Register</button></a>
<br>
&nbsp;
<h2>Our Mission</h2>
<p>Contrary to popular belief, Lorem Ipsum is not simply random
text. It has roots in a piece of classical Latin literature
from 45 BC, making it over 2000 years old. Richard
McClintock, a Latin professor at Hampden-Sydney College in
Virginia, looked up one of the more obscure Latin words,
consectetur, from a Lorem Ipsum passage, and going through
the cites of the word in classical literature, discovered
the undoubtable source. Lorem Ipsum comes from sections
1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum"
(The Extremes of Good and Evil) by Cicero, written in 45
BC. This book is a treatise on the theory of ethics, very
popular during the Renaissance. The first line of Lorem
Ipsum, "Lorem ipsum dolor sit amet..", comes from a line
in section 1.10.32.</p>
<br>
</div>
<div class=footer>
Contribute on <a href="https://github.com/b3yond/ticketfrei">GitHub!</a>
</div>
</div>
</body>

View File

@ -1,333 +0,0 @@
Authors ordered by first contribution
A list of current team members is available at http://jqueryui.com/about
Paul Bakaus <paul.bakaus@gmail.com>
Richard Worth <rdworth@gmail.com>
Yehuda Katz <wycats@gmail.com>
Sean Catchpole <sean@sunsean.com>
John Resig <jeresig@gmail.com>
Tane Piper <piper.tane@gmail.com>
Dmitri Gaskin <dmitrig01@gmail.com>
Klaus Hartl <klaus.hartl@gmail.com>
Stefan Petre <stefan.petre@gmail.com>
Gilles van den Hoven <gilles@webunity.nl>
Micheil Bryan Smith <micheil@brandedcode.com>
Jörn Zaefferer <joern.zaefferer@gmail.com>
Marc Grabanski <m@marcgrabanski.com>
Keith Wood <kbwood@iinet.com.au>
Brandon Aaron <brandon.aaron@gmail.com>
Scott González <scott.gonzalez@gmail.com>
Eduardo Lundgren <eduardolundgren@gmail.com>
Aaron Eisenberger <aaronchi@gmail.com>
Joan Piedra <theneojp@gmail.com>
Bruno Basto <b.basto@gmail.com>
Remy Sharp <remy@leftlogic.com>
Bohdan Ganicky <bohdan.ganicky@gmail.com>
David Bolter <david.bolter@gmail.com>
Chi Cheng <cloudream@gmail.com>
Ca-Phun Ung <pazu2k@gmail.com>
Ariel Flesler <aflesler@gmail.com>
Maggie Wachs <maggie@filamentgroup.com>
Scott Jehl <scottjehl@gmail.com>
Todd Parker <todd@filamentgroup.com>
Andrew Powell <andrew@shellscape.org>
Brant Burnett <btburnett3@gmail.com>
Douglas Neiner <doug@dougneiner.com>
Paul Irish <paul.irish@gmail.com>
Ralph Whitbeck <ralph.whitbeck@gmail.com>
Thibault Duplessis <thibault.duplessis@gmail.com>
Dominique Vincent <dominique.vincent@toitl.com>
Jack Hsu <jack.hsu@gmail.com>
Adam Sontag <ajpiano@ajpiano.com>
Carl Fürstenberg <carl@excito.com>
Kevin Dalman <development@allpro.net>
Alberto Fernández Capel <afcapel@gmail.com>
Jacek Jędrzejewski (http://jacek.jedrzejewski.name)
Ting Kuei <ting@kuei.com>
Samuel Cormier-Iijima <sam@chide.it>
Jon Palmer <jonspalmer@gmail.com>
Ben Hollis <bhollis@amazon.com>
Justin MacCarthy <Justin@Rubystars.biz>
Eyal Kobrigo <kobrigo@hotmail.com>
Tiago Freire <tiago.freire@gmail.com>
Diego Tres <diegotres@gmail.com>
Holger Rüprich <holger@rueprich.de>
Ziling Zhao <zilingzhao@gmail.com>
Mike Alsup <malsup@gmail.com>
Robson Braga Araujo <robsonbraga@gmail.com>
Pierre-Henri Ausseil <ph.ausseil@gmail.com>
Christopher McCulloh <cmcculloh@gmail.com>
Andrew Newcomb <ext.github@preceptsoftware.co.uk>
Lim Chee Aun <cheeaun@gmail.com>
Jorge Barreiro <yortx.barry@gmail.com>
Daniel Steigerwald <daniel@steigerwald.cz>
John Firebaugh <john_firebaugh@bigfix.com>
John Enters <github@darkdark.net>
Andrey Kapitcyn <ru.m157y@gmail.com>
Dmitry Petrov <dpetroff@gmail.com>
Eric Hynds <eric@hynds.net>
Chairat Sunthornwiphat <pipo@sixhead.com>
Josh Varner <josh.varner@gmail.com>
Stéphane Raimbault <stephane.raimbault@gmail.com>
Jay Merrifield <fracmak@gmail.com>
J. Ryan Stinnett <jryans@gmail.com>
Peter Heiberg <peter@heiberg.se>
Alex Dovenmuehle <adovenmuehle@gmail.com>
Jamie Gegerson <git@jamiegegerson.com>
Raymond Schwartz <skeetergraphics@gmail.com>
Phillip Barnes <philbar@gmail.com>
Kyle Wilkinson <kai@wikyd.org>
Khaled AlHourani <me@khaledalhourani.com>
Marian Rudzynski <mr@impaled.org>
Jean-Francois Remy <jeff@melix.org>
Doug Blood <dougblood@gmail.com>
Filippo Cavallarin <filippo.cavallarin@codseq.it>
Heiko Henning <heiko@thehennings.ch>
Aliaksandr Rahalevich <saksmlz@gmail.com>
Mario Visic <mario@mariovisic.com>
Xavi Ramirez <xavi.rmz@gmail.com>
Max Schnur <max.schnur@gmail.com>
Saji Nediyanchath <saji89@gmail.com>
Corey Frang <gnarf37@gmail.com>
Aaron Peterson <aaronp123@yahoo.com>
Ivan Peters <ivan@ivanpeters.com>
Mohamed Cherif Bouchelaghem <cherifbouchelaghem@yahoo.fr>
Marcos Sousa <falecomigo@marcossousa.com>
Michael DellaNoce <mdellanoce@mailtrust.com>
George Marshall <echosx@gmail.com>
Tobias Brunner <tobias@strongswan.org>
Martin Solli <msolli@gmail.com>
David Petersen <public@petersendidit.com>
Dan Heberden <danheberden@gmail.com>
William Kevin Manire <williamkmanire@gmail.com>
Gilmore Davidson <gilmoreorless@gmail.com>
Michael Wu <michaelmwu@gmail.com>
Adam Parod <mystic414@gmail.com>
Guillaume Gautreau <guillaume+github@ghusse.com>
Marcel Toele <EleotleCram@gmail.com>
Dan Streetman <ddstreet@ieee.org>
Matt Hoskins <matt@nipltd.com>
Giovanni Giacobbi <giovanni@giacobbi.net>
Kyle Florence <kyle.florence@gmail.com>
Pavol Hluchý <lopo@losys.sk>
Hans Hillen <hans.hillen@gmail.com>
Mark Johnson <virgofx@live.com>
Trey Hunner <treyhunner@gmail.com>
Shane Whittet <whittet@gmail.com>
Edward A Faulkner <ef@alum.mit.edu>
Adam Baratz <adam@adambaratz.com>
Kato Kazuyoshi <kato.kazuyoshi@gmail.com>
Eike Send <eike.send@gmail.com>
Kris Borchers <kris.borchers@gmail.com>
Eddie Monge <eddie@eddiemonge.com>
Israel Tsadok <itsadok@gmail.com>
Carson McDonald <carson@ioncannon.net>
Jason Davies <jason@jasondavies.com>
Garrison Locke <gplocke@gmail.com>
David Murdoch <david@davidmurdoch.com>
Benjamin Scott Boyle <benjamins.boyle@gmail.com>
Jesse Baird <jebaird@gmail.com>
Jonathan Vingiano <jvingiano@gmail.com>
Dylan Just <dev@ephox.com>
Hiroshi Tomita <tomykaira@gmail.com>
Glenn Goodrich <glenn.goodrich@gmail.com>
Tarafder Ashek-E-Elahi <mail.ashek@gmail.com>
Ryan Neufeld <ryan@neufeldmail.com>
Marc Neuwirth <marc.neuwirth@gmail.com>
Philip Graham <philip.robert.graham@gmail.com>
Benjamin Sterling <benjamin.sterling@kenzomedia.com>
Wesley Walser <waw325@gmail.com>
Kouhei Sutou <kou@clear-code.com>
Karl Kirch <karlkrch@gmail.com>
Chris Kelly <ckdake@ckdake.com>
Jason Oster <jay@kodewerx.org>
Felix Nagel <info@felixnagel.com>
Alexander Polomoshnov <alex.polomoshnov@gmail.com>
David Leal <dgleal@gmail.com>
Igor Milla <igor.fsp.milla@gmail.com>
Dave Methvin <dave.methvin@gmail.com>
Florian Gutmann <f.gutmann@chronimo.com>
Marwan Al Jubeh <marwan.aljubeh@gmail.com>
Milan Broum <midlis@googlemail.com>
Sebastian Sauer <info@dynpages.de>
Gaëtan Muller <m.gaetan89@gmail.com>
Michel Weimerskirch <michel@weimerskirch.net>
William Griffiths <william@ycymro.com>
Stojce Slavkovski <stojce@gmail.com>
David Soms <david.soms@gmail.com>
David De Sloovere <david.desloovere@outlook.com>
Michael P. Jung <michael.jung@terreon.de>
Shannon Pekary <spekary@gmail.com>
Dan Wellman <danwellman@hotmail.com>
Matthew Edward Hutton <meh@corefiling.co.uk>
James Khoury <james@jameskhoury.com>
Rob Loach <robloach@gmail.com>
Alberto Monteiro <betimbrasil@gmail.com>
Alex Rhea <alex.rhea@gmail.com>
Krzysztof Rosiński <rozwell69@gmail.com>
Ryan Olton <oltonr@gmail.com>
Genie <386@mail.com>
Rick Waldron <waldron.rick@gmail.com>
Ian Simpson <spoonlikesham@gmail.com>
Lev Kitsis <spam4lev@gmail.com>
TJ VanToll <tj.vantoll@gmail.com>
Justin Domnitz <jdomnitz@gmail.com>
Douglas Cerna <douglascerna@yahoo.com>
Bert ter Heide <bertjh@hotmail.com>
Jasvir Nagra <jasvir@gmail.com>
Yuriy Khabarov <13real008@gmail.com>
Harri Kilpiö <harri.kilpio@gmail.com>
Lado Lomidze <lado.lomidze@gmail.com>
Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
Simon Sattes <simon.sattes@gmail.com>
Jo Liss <joliss42@gmail.com>
Guntupalli Karunakar <karunakarg@yahoo.com>
Shahyar Ghobadpour <shahyar@gmail.com>
Lukasz Lipinski <uzza17@gmail.com>
Timo Tijhof <krinklemail@gmail.com>
Jason Moon <jmoon@socialcast.com>
Martin Frost <martinf55@hotmail.com>
Eneko Illarramendi <eneko@illarra.com>
EungJun Yi <semtlenori@gmail.com>
Courtland Allen <courtlandallen@gmail.com>
Viktar Varvanovich <non4eg@gmail.com>
Danny Trunk <dtrunk90@gmail.com>
Pavel Stetina <pavel.stetina@nangu.tv>
Michael Stay <metaweta@gmail.com>
Steven Roussey <sroussey@gmail.com>
Michael Hollis <hollis21@gmail.com>
Lee Rowlands <lee.rowlands@previousnext.com.au>
Timmy Willison <timmywillisn@gmail.com>
Karl Swedberg <kswedberg@gmail.com>
Baoju Yuan <the_guy_1987@hotmail.com>
Maciej Mroziński <maciej.k.mrozinski@gmail.com>
Luis Dalmolin <luis.nh@gmail.com>
Mark Aaron Shirley <maspwr@gmail.com>
Martin Hoch <martin@fidion.de>
Jiayi Yang <tr870829@gmail.com>
Philipp Benjamin Köppchen <xgxtpbk@gws.ms>
Sindre Sorhus <sindresorhus@gmail.com>
Bernhard Sirlinger <bernhard.sirlinger@tele2.de>
Jared A. Scheel <jared@jaredscheel.com>
Rafael Xavier de Souza <rxaviers@gmail.com>
John Chen <zhang.z.chen@intel.com>
Robert Beuligmann <robertbeuligmann@gmail.com>
Dale Kocian <dale.kocian@gmail.com>
Mike Sherov <mike.sherov@gmail.com>
Andrew Couch <andy@couchand.com>
Marc-Andre Lafortune <github@marc-andre.ca>
Nate Eagle <nate.eagle@teamaol.com>
David Souther <davidsouther@gmail.com>
Mathias Stenbom <mathias@stenbom.com>
Sergey Kartashov <ebishkek@yandex.ru>
Avinash R <nashpapa@gmail.com>
Ethan Romba <ethanromba@gmail.com>
Cory Gackenheimer <cory.gack@gmail.com>
Juan Pablo Kaniefsky <jpkaniefsky@gmail.com>
Roman Salnikov <bardt.dz@gmail.com>
Anika Henke <anika@selfthinker.org>
Samuel Bovée <samycookie2000@yahoo.fr>
Fabrício Matté <ult_combo@hotmail.com>
Viktor Kojouharov <vkojouharov@gmail.com>
Pawel Maruszczyk (http://hrabstwo.net)
Pavel Selitskas <p.selitskas@gmail.com>
Bjørn Johansen <post@bjornjohansen.no>
Matthieu Penant <thieum22@hotmail.com>
Dominic Barnes <dominic@dbarnes.info>
David Sullivan <david.sullivan@gmail.com>
Thomas Jaggi <thomas@responsive.ch>
Vahid Sohrabloo <vahid4134@gmail.com>
Travis Carden <travis.carden@gmail.com>
Bruno M. Custódio <bruno@brunomcustodio.com>
Nathanael Silverman <nathanael.silverman@gmail.com>
Christian Wenz <christian@wenz.org>
Steve Urmston <steve@urm.st>
Zaven Muradyan <megalivoithos@gmail.com>
Woody Gilk <shadowhand@deviantart.com>
Zbigniew Motyka <zbigniew.motyka@gmail.com>
Suhail Alkowaileet <xsoh.k7@gmail.com>
Toshi MARUYAMA <marutosijp2@yahoo.co.jp>
David Hansen <hansede@gmail.com>
Brian Grinstead <briangrinstead@gmail.com>
Christian Klammer <christian314159@gmail.com>
Steven Luscher <jquerycla@steveluscher.com>
Gan Eng Chin <engchin.gan@gmail.com>
Gabriel Schulhof <gabriel.schulhof@intel.com>
Alexander Schmitz <arschmitz@gmail.com>
Vilhjálmur Skúlason <vis@dmm.is>
Siebrand Mazeland <siebrand@kitano.nl>
Mohsen Ekhtiari <mohsenekhtiari@yahoo.com>
Pere Orga <gotrunks@gmail.com>
Jasper de Groot <mail@ugomobi.com>
Stephane Deschamps <stephane.deschamps@gmail.com>
Jyoti Deka <dekajp@gmail.com>
Andrei Picus <office.nightcrawler@gmail.com>
Ondrej Novy <novy@ondrej.org>
Jacob McCutcheon <jacob.mccutcheon@gmail.com>
Monika Piotrowicz <monika.piotrowicz@gmail.com>
Imants Horsts <imants.horsts@inbox.lv>
Eric Dahl <eric.c.dahl@gmail.com>
Dave Stein <dave@behance.com>
Dylan Barrell <dylan@barrell.com>
Daniel DeGroff <djdegroff@gmail.com>
Michael Wiencek <mwtuea@gmail.com>
Thomas Meyer <meyertee@gmail.com>
Ruslan Yakhyaev <ruslan@ruslan.io>
Brian J. Dowling <bjd-dev@simplicity.net>
Ben Higgins <ben@extrahop.com>
Yermo Lamers <yml@yml.com>
Patrick Stapleton <github@gdi2290.com>
Trisha Crowley <trisha.crowley@gmail.com>
Usman Akeju <akeju00+github@gmail.com>
Rodrigo Menezes <rod333@gmail.com>
Jacques Perrault <jacques_perrault@us.ibm.com>
Frederik Elvhage <frederik.elvhage@googlemail.com>
Will Holley <willholley@gmail.com>
Uri Gilad <antishok@gmail.com>
Richard Gibson <richard.gibson@gmail.com>
Simen Bekkhus <sbekkhus91@gmail.com>
Chen Eshchar <eshcharc@gmail.com>
Bruno Pérel <brunoperel@gmail.com>
Mohammed Alshehri <m@dralshehri.com>
Lisa Seacat DeLuca <ldeluca@us.ibm.com>
Anne-Gaelle Colom <coloma@westminster.ac.uk>
Adam Foster <slimfoster@gmail.com>
Luke Page <luke.a.page@gmail.com>
Daniel Owens <daniel@matchstickmixup.com>
Michael Orchard <morchard@scottlogic.co.uk>
Marcus Warren <marcus@envoke.com>
Nils Heuermann <nils@world-of-scripts.de>
Marco Ziech <marco@ziech.net>
Patricia Juarez <patrixd@gmail.com>
Ben Mosher <me@benmosher.com>
Ablay Keldibek <atomio.ak@gmail.com>
Thomas Applencourt <thomas.applencourt@irsamc.ups-tlse.fr>
Jiabao Wu <jiabao.foss@gmail.com>
Eric Lee Carraway <github@ericcarraway.com>
Victor Homyakov <vkhomyackov@gmail.com>
Myeongjin Lee <aranet100@gmail.com>
Liran Sharir <lsharir@gmail.com>
Weston Ruter <weston@xwp.co>
Mani Mishra <manimishra902@gmail.com>
Hannah Methvin <hannahmethvin@gmail.com>
Leonardo Balter <leonardo.balter@gmail.com>
Benjamin Albert <benjamin_a5@yahoo.com>
Michał Gołębiowski <m.goleb@gmail.com>
Alyosha Pushak <alyosha.pushak@gmail.com>
Fahad Ahmad <fahadahmad41@hotmail.com>
Matt Brundage <github@mattbrundage.com>
Francesc Baeta <francesc.baeta@gmail.com>
Piotr Baran <piotros@wp.pl>
Mukul Hase <mukulhase@gmail.com>
Konstantin Dinev <kdinev@mail.bw.edu>
Rand Scullard <rand@randscullard.com>
Dan Strohl <dan@wjcg.net>
Maksim Ryzhikov <rv.maksim@gmail.com>
Amine HADDAD <haddad@allegorie.tv>
Amanpreet Singh <apsdehal@gmail.com>
Alexey Balchunas <bleshik@gmail.com>
Peter Kehl <peter.kehl@gmail.com>
Peter Dave Hello <hsu@peterdavehello.org>
Johannes Schäfer <johnschaefer@gmx.de>
Ville Skyttä <ville.skytta@iki.fi>
Ryan Oriecuia <ryan.oriecuia@visioncritical.com>

View File

@ -1,43 +0,0 @@
Copyright jQuery Foundation and other contributors, https://jquery.org/
This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available at https://github.com/jquery/jquery-ui
The following license applies to all parts of this software except as
documented below:
====
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
====
Copyright and related rights for sample code are waived via CC0. Sample
code is defined as all source code contained within the demos directory.
CC0: http://creativecommons.org/publicdomain/zero/1.0/
====
All files located in the node_modules and external directories are
externally maintained libraries used by this software which have their
own licenses; we recommend you read them, as their terms may differ from
the terms above.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,559 +0,0 @@
<!doctype html>
<html lang="us">
<head>
<meta charset="utf-8">
<title>jQuery UI Example Page</title>
<link href="jquery-ui.css" rel="stylesheet">
<style>
body{
font-family: "Trebuchet MS", sans-serif;
margin: 50px;
}
.demoHeaders {
margin-top: 2em;
}
#dialog-link {
padding: .4em 1em .4em 20px;
text-decoration: none;
position: relative;
}
#dialog-link span.ui-icon {
margin: 0 5px 0 0;
position: absolute;
left: .2em;
top: 50%;
margin-top: -8px;
}
#icons {
margin: 0;
padding: 0;
}
#icons li {
margin: 2px;
position: relative;
padding: 4px 0;
cursor: pointer;
float: left;
list-style: none;
}
#icons span.ui-icon {
float: left;
margin: 0 4px;
}
.fakewindowcontain .ui-widget-overlay {
position: absolute;
}
select {
width: 200px;
}
</style>
</head>
<body>
<h1>Welcome to jQuery UI!</h1>
<div class="ui-widget">
<p>This page demonstrates the widgets and theme you selected in Download Builder. Please make sure you are using them with a compatible jQuery version.</p>
</div>
<h1>YOUR COMPONENTS:</h1>
<!-- Accordion -->
<h2 class="demoHeaders">Accordion</h2>
<div id="accordion">
<h3>First</h3>
<div>Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet.</div>
<h3>Second</h3>
<div>Phasellus mattis tincidunt nibh.</div>
<h3>Third</h3>
<div>Nam dui erat, auctor a, dignissim quis.</div>
</div>
<!-- Autocomplete -->
<h2 class="demoHeaders">Autocomplete</h2>
<div>
<input id="autocomplete" title="type &quot;a&quot;">
</div>
<!-- Button -->
<h2 class="demoHeaders">Button</h2>
<button id="button">A button element</button>
<button id="button-icon">An icon-only button</button>
<!-- Checkboxradio -->
<h2 class="demoHeaders">Checkboxradio</h2>
<form style="margin-top: 1em;">
<div id="radioset">
<input type="radio" id="radio1" name="radio"><label for="radio1">Choice 1</label>
<input type="radio" id="radio2" name="radio" checked="checked"><label for="radio2">Choice 2</label>
<input type="radio" id="radio3" name="radio"><label for="radio3">Choice 3</label>
</div>
</form>
<!-- Controlgroup -->
<h2 class="demoHeaders">Controlgroup</h2>
<fieldset>
<legend>Rental Car</legend>
<div id="controlgroup">
<select id="car-type">
<option>Compact car</option>
<option>Midsize car</option>
<option>Full size car</option>
<option>SUV</option>
<option>Luxury</option>
<option>Truck</option>
<option>Van</option>
</select>
<label for="transmission-standard">Standard</label>
<input type="radio" name="transmission" id="transmission-standard">
<label for="transmission-automatic">Automatic</label>
<input type="radio" name="transmission" id="transmission-automatic">
<label for="insurance">Insurance</label>
<input type="checkbox" name="insurance" id="insurance">
<label for="horizontal-spinner" class="ui-controlgroup-label"># of cars</label>
<input id="horizontal-spinner" class="ui-spinner-input">
<button>Book Now!</button>
</div>
</fieldset>
<!-- Tabs -->
<h2 class="demoHeaders">Tabs</h2>
<div id="tabs">
<ul>
<li><a href="#tabs-1">First</a></li>
<li><a href="#tabs-2">Second</a></li>
<li><a href="#tabs-3">Third</a></li>
</ul>
<div id="tabs-1">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</div>
<div id="tabs-2">Phasellus mattis tincidunt nibh. Cras orci urna, blandit id, pretium vel, aliquet ornare, felis. Maecenas scelerisque sem non nisl. Fusce sed lorem in enim dictum bibendum.</div>
<div id="tabs-3">Nam dui erat, auctor a, dignissim quis, sollicitudin eu, felis. Pellentesque nisi urna, interdum eget, sagittis et, consequat vestibulum, lacus. Mauris porttitor ullamcorper augue.</div>
</div>
<h2 class="demoHeaders">Dialog</h2>
<p>
<button id="dialog-link" class="ui-button ui-corner-all ui-widget">
<span class="ui-icon ui-icon-newwin"></span>Open Dialog
</button>
</p>
<h2 class="demoHeaders">Overlay and Shadow Classes</h2>
<div style="position: relative; width: 96%; height: 200px; padding:1% 2%; overflow:hidden;" class="fakewindowcontain">
<p>Lorem ipsum dolor sit amet, Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. </p><p>Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. </p><p>Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. </p><p>Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. Aliquam ante. </p><p>Suspendisse scelerisque dui nec velit. Duis augue augue, gravida euismod, vulputate ac, facilisis id, sem. Morbi in orci. Nulla purus lacus, pulvinar vel, malesuada ac, mattis nec, quam. Nam molestie scelerisque quam. Nullam feugiat cursus lacus.orem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero risus, commodo vitae, pharetra mollis, posuere eu, pede. Nulla nec tortor. Donec id elit quis purus consectetur consequat. Nam congue semper tellus. Sed erat dolor, dapibus sit amet, venenatis ornare, ultrices ut, nisi. </p>
<!-- ui-dialog -->
<div class="ui-widget-overlay ui-front"></div>
<div style="position: absolute; width: 320px; left: 50px; top: 30px; padding: 1.2em" class="ui-widget ui-front ui-widget-content ui-corner-all ui-widget-shadow">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</div>
</div>
<!-- ui-dialog -->
<div id="dialog" title="Dialog Title">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
<h2 class="demoHeaders">Framework Icons (content color preview)</h2>
<ul id="icons" class="ui-widget ui-helper-clearfix">
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-n"><span class="ui-icon ui-icon-caret-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-ne"><span class="ui-icon ui-icon-caret-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-e"><span class="ui-icon ui-icon-caret-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-se"><span class="ui-icon ui-icon-caret-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-s"><span class="ui-icon ui-icon-caret-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-sw"><span class="ui-icon ui-icon-caret-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-w"><span class="ui-icon ui-icon-caret-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-1-nw"><span class="ui-icon ui-icon-caret-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-n-s"><span class="ui-icon ui-icon-caret-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-caret-2-e-w"><span class="ui-icon ui-icon-caret-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-n"><span class="ui-icon ui-icon-triangle-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-ne"><span class="ui-icon ui-icon-triangle-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-e"><span class="ui-icon ui-icon-triangle-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-se"><span class="ui-icon ui-icon-triangle-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-s"><span class="ui-icon ui-icon-triangle-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-sw"><span class="ui-icon ui-icon-triangle-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-w"><span class="ui-icon ui-icon-triangle-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-1-nw"><span class="ui-icon ui-icon-triangle-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-n-s"><span class="ui-icon ui-icon-triangle-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-triangle-2-e-w"><span class="ui-icon ui-icon-triangle-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-n"><span class="ui-icon ui-icon-arrow-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-ne"><span class="ui-icon ui-icon-arrow-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-e"><span class="ui-icon ui-icon-arrow-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-se"><span class="ui-icon ui-icon-arrow-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-s"><span class="ui-icon ui-icon-arrow-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-sw"><span class="ui-icon ui-icon-arrow-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-w"><span class="ui-icon ui-icon-arrow-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-1-nw"><span class="ui-icon ui-icon-arrow-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-n-s"><span class="ui-icon ui-icon-arrow-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-ne-sw"><span class="ui-icon ui-icon-arrow-2-ne-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-e-w"><span class="ui-icon ui-icon-arrow-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-2-se-nw"><span class="ui-icon ui-icon-arrow-2-se-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-n"><span class="ui-icon ui-icon-arrowstop-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-e"><span class="ui-icon ui-icon-arrowstop-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-s"><span class="ui-icon ui-icon-arrowstop-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowstop-1-w"><span class="ui-icon ui-icon-arrowstop-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-n"><span class="ui-icon ui-icon-arrowthick-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-ne"><span class="ui-icon ui-icon-arrowthick-1-ne"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-e"><span class="ui-icon ui-icon-arrowthick-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-se"><span class="ui-icon ui-icon-arrowthick-1-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-s"><span class="ui-icon ui-icon-arrowthick-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-sw"><span class="ui-icon ui-icon-arrowthick-1-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-w"><span class="ui-icon ui-icon-arrowthick-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-1-nw"><span class="ui-icon ui-icon-arrowthick-1-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-n-s"><span class="ui-icon ui-icon-arrowthick-2-n-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-ne-sw"><span class="ui-icon ui-icon-arrowthick-2-ne-sw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-e-w"><span class="ui-icon ui-icon-arrowthick-2-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthick-2-se-nw"><span class="ui-icon ui-icon-arrowthick-2-se-nw"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-n"><span class="ui-icon ui-icon-arrowthickstop-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-e"><span class="ui-icon ui-icon-arrowthickstop-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-s"><span class="ui-icon ui-icon-arrowthickstop-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowthickstop-1-w"><span class="ui-icon ui-icon-arrowthickstop-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-w"><span class="ui-icon ui-icon-arrowreturnthick-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-n"><span class="ui-icon ui-icon-arrowreturnthick-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-e"><span class="ui-icon ui-icon-arrowreturnthick-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturnthick-1-s"><span class="ui-icon ui-icon-arrowreturnthick-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-w"><span class="ui-icon ui-icon-arrowreturn-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-n"><span class="ui-icon ui-icon-arrowreturn-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-e"><span class="ui-icon ui-icon-arrowreturn-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowreturn-1-s"><span class="ui-icon ui-icon-arrowreturn-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-w"><span class="ui-icon ui-icon-arrowrefresh-1-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-n"><span class="ui-icon ui-icon-arrowrefresh-1-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-e"><span class="ui-icon ui-icon-arrowrefresh-1-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrowrefresh-1-s"><span class="ui-icon ui-icon-arrowrefresh-1-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4"><span class="ui-icon ui-icon-arrow-4"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-arrow-4-diag"><span class="ui-icon ui-icon-arrow-4-diag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-extlink"><span class="ui-icon ui-icon-extlink"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-newwin"><span class="ui-icon ui-icon-newwin"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-refresh"><span class="ui-icon ui-icon-refresh"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-shuffle"><span class="ui-icon ui-icon-shuffle"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-transfer-e-w"><span class="ui-icon ui-icon-transfer-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-transferthick-e-w"><span class="ui-icon ui-icon-transferthick-e-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-collapsed"><span class="ui-icon ui-icon-folder-collapsed"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-folder-open"><span class="ui-icon ui-icon-folder-open"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-document"><span class="ui-icon ui-icon-document"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-document-b"><span class="ui-icon ui-icon-document-b"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-note"><span class="ui-icon ui-icon-note"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-closed"><span class="ui-icon ui-icon-mail-closed"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-mail-open"><span class="ui-icon ui-icon-mail-open"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-suitcase"><span class="ui-icon ui-icon-suitcase"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-comment"><span class="ui-icon ui-icon-comment"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-person"><span class="ui-icon ui-icon-person"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-print"><span class="ui-icon ui-icon-print"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-trash"><span class="ui-icon ui-icon-trash"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-locked"><span class="ui-icon ui-icon-locked"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-unlocked"><span class="ui-icon ui-icon-unlocked"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-bookmark"><span class="ui-icon ui-icon-bookmark"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-tag"><span class="ui-icon ui-icon-tag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-home"><span class="ui-icon ui-icon-home"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-flag"><span class="ui-icon ui-icon-flag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-calculator"><span class="ui-icon ui-icon-calculator"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-cart"><span class="ui-icon ui-icon-cart"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pencil"><span class="ui-icon ui-icon-pencil"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-clock"><span class="ui-icon ui-icon-clock"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-disk"><span class="ui-icon ui-icon-disk"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-calendar"><span class="ui-icon ui-icon-calendar"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomin"><span class="ui-icon ui-icon-zoomin"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-zoomout"><span class="ui-icon ui-icon-zoomout"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-search"><span class="ui-icon ui-icon-search"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-wrench"><span class="ui-icon ui-icon-wrench"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-gear"><span class="ui-icon ui-icon-gear"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-heart"><span class="ui-icon ui-icon-heart"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-star"><span class="ui-icon ui-icon-star"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-link"><span class="ui-icon ui-icon-link"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-cancel"><span class="ui-icon ui-icon-cancel"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-plus"><span class="ui-icon ui-icon-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-plusthick"><span class="ui-icon ui-icon-plusthick"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-minus"><span class="ui-icon ui-icon-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-minusthick"><span class="ui-icon ui-icon-minusthick"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-close"><span class="ui-icon ui-icon-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-closethick"><span class="ui-icon ui-icon-closethick"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-key"><span class="ui-icon ui-icon-key"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-lightbulb"><span class="ui-icon ui-icon-lightbulb"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-scissors"><span class="ui-icon ui-icon-scissors"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-clipboard"><span class="ui-icon ui-icon-clipboard"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-copy"><span class="ui-icon ui-icon-copy"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-contact"><span class="ui-icon ui-icon-contact"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-image"><span class="ui-icon ui-icon-image"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-video"><span class="ui-icon ui-icon-video"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-script"><span class="ui-icon ui-icon-script"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-alert"><span class="ui-icon ui-icon-alert"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-info"><span class="ui-icon ui-icon-info"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-notice"><span class="ui-icon ui-icon-notice"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-help"><span class="ui-icon ui-icon-help"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-check"><span class="ui-icon ui-icon-check"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-bullet"><span class="ui-icon ui-icon-bullet"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-off"><span class="ui-icon ui-icon-radio-off"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-radio-on"><span class="ui-icon ui-icon-radio-on"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-w"><span class="ui-icon ui-icon-pin-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pin-s"><span class="ui-icon ui-icon-pin-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-play"><span class="ui-icon ui-icon-play"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-pause"><span class="ui-icon ui-icon-pause"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-next"><span class="ui-icon ui-icon-seek-next"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-prev"><span class="ui-icon ui-icon-seek-prev"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-end"><span class="ui-icon ui-icon-seek-end"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-seek-first"><span class="ui-icon ui-icon-seek-first"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-stop"><span class="ui-icon ui-icon-stop"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-eject"><span class="ui-icon ui-icon-eject"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-off"><span class="ui-icon ui-icon-volume-off"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-volume-on"><span class="ui-icon ui-icon-volume-on"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-power"><span class="ui-icon ui-icon-power"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-signal-diag"><span class="ui-icon ui-icon-signal-diag"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-signal"><span class="ui-icon ui-icon-signal"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-0"><span class="ui-icon ui-icon-battery-0"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-1"><span class="ui-icon ui-icon-battery-1"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-2"><span class="ui-icon ui-icon-battery-2"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-battery-3"><span class="ui-icon ui-icon-battery-3"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-plus"><span class="ui-icon ui-icon-circle-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-minus"><span class="ui-icon ui-icon-circle-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-close"><span class="ui-icon ui-icon-circle-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-e"><span class="ui-icon ui-icon-circle-triangle-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-s"><span class="ui-icon ui-icon-circle-triangle-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-w"><span class="ui-icon ui-icon-circle-triangle-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-triangle-n"><span class="ui-icon ui-icon-circle-triangle-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-e"><span class="ui-icon ui-icon-circle-arrow-e"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-s"><span class="ui-icon ui-icon-circle-arrow-s"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-w"><span class="ui-icon ui-icon-circle-arrow-w"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-arrow-n"><span class="ui-icon ui-icon-circle-arrow-n"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomin"><span class="ui-icon ui-icon-circle-zoomin"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-zoomout"><span class="ui-icon ui-icon-circle-zoomout"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circle-check"><span class="ui-icon ui-icon-circle-check"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-plus"><span class="ui-icon ui-icon-circlesmall-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-minus"><span class="ui-icon ui-icon-circlesmall-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-circlesmall-close"><span class="ui-icon ui-icon-circlesmall-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-plus"><span class="ui-icon ui-icon-squaresmall-plus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-minus"><span class="ui-icon ui-icon-squaresmall-minus"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-squaresmall-close"><span class="ui-icon ui-icon-squaresmall-close"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-vertical"><span class="ui-icon ui-icon-grip-dotted-vertical"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-dotted-horizontal"><span class="ui-icon ui-icon-grip-dotted-horizontal"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-vertical"><span class="ui-icon ui-icon-grip-solid-vertical"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-solid-horizontal"><span class="ui-icon ui-icon-grip-solid-horizontal"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-gripsmall-diagonal-se"><span class="ui-icon ui-icon-gripsmall-diagonal-se"></span></li>
<li class="ui-state-default ui-corner-all" title=".ui-icon-grip-diagonal-se"><span class="ui-icon ui-icon-grip-diagonal-se"></span></li>
</ul>
<!-- Slider -->
<h2 class="demoHeaders">Slider</h2>
<div id="slider"></div>
<!-- Datepicker -->
<h2 class="demoHeaders">Datepicker</h2>
<div id="datepicker"></div>
<!-- Progressbar -->
<h2 class="demoHeaders">Progressbar</h2>
<div id="progressbar"></div>
<!-- Progressbar -->
<h2 class="demoHeaders">Selectmenu</h2>
<select id="selectmenu">
<option>Slower</option>
<option>Slow</option>
<option selected="selected">Medium</option>
<option>Fast</option>
<option>Faster</option>
</select>
<!-- Spinner -->
<h2 class="demoHeaders">Spinner</h2>
<input id="spinner">
<!-- Menu -->
<h2 class="demoHeaders">Menu</h2>
<ul style="width:100px;" id="menu">
<li><div>Item 1</div></li>
<li><div>Item 2</div></li>
<li><div>Item 3</div>
<ul>
<li><div>Item 3-1</div></li>
<li><div>Item 3-2</div></li>
<li><div>Item 3-3</div></li>
<li><div>Item 3-4</div></li>
<li><div>Item 3-5</div></li>
</ul>
</li>
<li><div>Item 4</div></li>
<li><div>Item 5</div></li>
</ul>
<!-- Tooltip -->
<h2 class="demoHeaders">Tooltip</h2>
<p id="tooltip">
<a href="#" title="That&apos;s what this widget is">Tooltips</a> can be attached to any element. When you hover
the element with your mouse, the title attribute is displayed in a little box next to the element, just like a native tooltip.
</p>
<!-- Highlight / Error -->
<h2 class="demoHeaders">Highlight / Error</h2>
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
<strong>Hey!</strong> Sample ui-state-highlight style.</p>
</div>
</div>
<br>
<div class="ui-widget">
<div class="ui-state-error ui-corner-all" style="padding: 0 .7em;">
<p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>
<strong>Alert:</strong> Sample ui-state-error style.</p>
</div>
</div>
<script src="external/jquery/jquery.js"></script>
<script src="jquery-ui.js"></script>
<script>
$( "#accordion" ).accordion();
var availableTags = [
"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"ColdFusion",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"
];
$( "#autocomplete" ).autocomplete({
source: availableTags
});
$( "#button" ).button();
$( "#button-icon" ).button({
icon: "ui-icon-gear",
showLabel: false
});
$( "#radioset" ).buttonset();
$( "#controlgroup" ).controlgroup();
$( "#tabs" ).tabs();
$( "#dialog" ).dialog({
autoOpen: false,
width: 400,
buttons: [
{
text: "Ok",
click: function() {
$( this ).dialog( "close" );
}
},
{
text: "Cancel",
click: function() {
$( this ).dialog( "close" );
}
}
]
});
// Link to open the dialog
$( "#dialog-link" ).click(function( event ) {
$( "#dialog" ).dialog( "open" );
event.preventDefault();
});
$( "#datepicker" ).datepicker({
inline: true
});
$( "#slider" ).slider({
range: true,
values: [ 17, 67 ]
});
$( "#progressbar" ).progressbar({
value: 20
});
$( "#spinner" ).spinner();
$( "#menu" ).menu();
$( "#tooltip" ).tooltip();
$( "#selectmenu" ).selectmenu();
// Hover states on the static widgets
$( "#dialog-link, #icons li" ).hover(
function() {
$( this ).addClass( "ui-state-hover" );
},
function() {
$( this ).removeClass( "ui-state-hover" );
}
);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,886 +0,0 @@
/*!
* jQuery UI CSS Framework 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/theming/
*/
/* Layout helpers
----------------------------------*/
.ui-helper-hidden {
display: none;
}
.ui-helper-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.ui-helper-reset {
margin: 0;
padding: 0;
border: 0;
outline: 0;
line-height: 1.3;
text-decoration: none;
font-size: 100%;
list-style: none;
}
.ui-helper-clearfix:before,
.ui-helper-clearfix:after {
content: "";
display: table;
border-collapse: collapse;
}
.ui-helper-clearfix:after {
clear: both;
}
.ui-helper-zfix {
width: 100%;
height: 100%;
top: 0;
left: 0;
position: absolute;
opacity: 0;
filter:Alpha(Opacity=0); /* support: IE8 */
}
.ui-front {
z-index: 100;
}
/* Interaction Cues
----------------------------------*/
.ui-state-disabled {
cursor: default !important;
pointer-events: none;
}
/* Icons
----------------------------------*/
.ui-icon {
display: inline-block;
vertical-align: middle;
margin-top: -.25em;
position: relative;
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
}
.ui-widget-icon-block {
left: 50%;
margin-left: -8px;
display: block;
}
/* Misc visuals
----------------------------------*/
/* Overlays */
.ui-widget-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.ui-accordion .ui-accordion-header {
display: block;
cursor: pointer;
position: relative;
margin: 2px 0 0 0;
padding: .5em .5em .5em .7em;
font-size: 100%;
}
.ui-accordion .ui-accordion-content {
padding: 1em 2.2em;
border-top: 0;
overflow: auto;
}
.ui-autocomplete {
position: absolute;
top: 0;
left: 0;
cursor: default;
}
.ui-menu {
list-style: none;
padding: 0;
margin: 0;
display: block;
outline: 0;
}
.ui-menu .ui-menu {
position: absolute;
}
.ui-menu .ui-menu-item {
margin: 0;
cursor: pointer;
/* support: IE10, see #8844 */
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
}
.ui-menu .ui-menu-item-wrapper {
position: relative;
padding: 3px 1em 3px .4em;
}
.ui-menu .ui-menu-divider {
margin: 5px 0;
height: 0;
font-size: 0;
line-height: 0;
border-width: 1px 0 0 0;
}
.ui-menu .ui-state-focus,
.ui-menu .ui-state-active {
margin: -1px;
}
/* icon support */
.ui-menu-icons {
position: relative;
}
.ui-menu-icons .ui-menu-item-wrapper {
padding-left: 2em;
}
/* left-aligned */
.ui-menu .ui-icon {
position: absolute;
top: 0;
bottom: 0;
left: .2em;
margin: auto 0;
}
/* right-aligned */
.ui-menu .ui-menu-icon {
left: auto;
right: 0;
}
.ui-button {
padding: .4em 1em;
display: inline-block;
position: relative;
line-height: normal;
margin-right: .1em;
cursor: pointer;
vertical-align: middle;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
/* Support: IE <= 11 */
overflow: visible;
}
.ui-button,
.ui-button:link,
.ui-button:visited,
.ui-button:hover,
.ui-button:active {
text-decoration: none;
}
/* to make room for the icon, a width needs to be set here */
.ui-button-icon-only {
width: 2em;
box-sizing: border-box;
text-indent: -9999px;
white-space: nowrap;
}
/* no icon support for input elements */
input.ui-button.ui-button-icon-only {
text-indent: 0;
}
/* button icon element(s) */
.ui-button-icon-only .ui-icon {
position: absolute;
top: 50%;
left: 50%;
margin-top: -8px;
margin-left: -8px;
}
.ui-button.ui-icon-notext .ui-icon {
padding: 0;
width: 2.1em;
height: 2.1em;
text-indent: -9999px;
white-space: nowrap;
}
input.ui-button.ui-icon-notext .ui-icon {
width: auto;
height: auto;
text-indent: 0;
white-space: normal;
padding: .4em 1em;
}
/* workarounds */
/* Support: Firefox 5 - 40 */
input.ui-button::-moz-focus-inner,
button.ui-button::-moz-focus-inner {
border: 0;
padding: 0;
}
.ui-controlgroup {
vertical-align: middle;
display: inline-block;
}
.ui-controlgroup > .ui-controlgroup-item {
float: left;
margin-left: 0;
margin-right: 0;
}
.ui-controlgroup > .ui-controlgroup-item:focus,
.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus {
z-index: 9999;
}
.ui-controlgroup-vertical > .ui-controlgroup-item {
display: block;
float: none;
width: 100%;
margin-top: 0;
margin-bottom: 0;
text-align: left;
}
.ui-controlgroup-vertical .ui-controlgroup-item {
box-sizing: border-box;
}
.ui-controlgroup .ui-controlgroup-label {
padding: .4em 1em;
}
.ui-controlgroup .ui-controlgroup-label span {
font-size: 80%;
}
.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item {
border-left: none;
}
.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item {
border-top: none;
}
.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content {
border-right: none;
}
.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content {
border-bottom: none;
}
/* Spinner specific style fixes */
.ui-controlgroup-vertical .ui-spinner-input {
/* Support: IE8 only, Android < 4.4 only */
width: 75%;
width: calc( 100% - 2.4em );
}
.ui-controlgroup-vertical .ui-spinner .ui-spinner-up {
border-top-style: solid;
}
.ui-checkboxradio-label .ui-icon-background {
box-shadow: inset 1px 1px 1px #ccc;
border-radius: .12em;
border: none;
}
.ui-checkboxradio-radio-label .ui-icon-background {
width: 16px;
height: 16px;
border-radius: 1em;
overflow: visible;
border: none;
}
.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,
.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon {
background-image: none;
width: 8px;
height: 8px;
border-width: 4px;
border-style: solid;
}
.ui-checkboxradio-disabled {
pointer-events: none;
}
.ui-datepicker {
width: 17em;
padding: .2em .2em 0;
display: none;
}
.ui-datepicker .ui-datepicker-header {
position: relative;
padding: .2em 0;
}
.ui-datepicker .ui-datepicker-prev,
.ui-datepicker .ui-datepicker-next {
position: absolute;
top: 2px;
width: 1.8em;
height: 1.8em;
}
.ui-datepicker .ui-datepicker-prev-hover,
.ui-datepicker .ui-datepicker-next-hover {
top: 1px;
}
.ui-datepicker .ui-datepicker-prev {
left: 2px;
}
.ui-datepicker .ui-datepicker-next {
right: 2px;
}
.ui-datepicker .ui-datepicker-prev-hover {
left: 1px;
}
.ui-datepicker .ui-datepicker-next-hover {
right: 1px;
}
.ui-datepicker .ui-datepicker-prev span,
.ui-datepicker .ui-datepicker-next span {
display: block;
position: absolute;
left: 50%;
margin-left: -8px;
top: 50%;
margin-top: -8px;
}
.ui-datepicker .ui-datepicker-title {
margin: 0 2.3em;
line-height: 1.8em;
text-align: center;
}
.ui-datepicker .ui-datepicker-title select {
font-size: 1em;
margin: 1px 0;
}
.ui-datepicker select.ui-datepicker-month,
.ui-datepicker select.ui-datepicker-year {
width: 45%;
}
.ui-datepicker table {
width: 100%;
font-size: .9em;
border-collapse: collapse;
margin: 0 0 .4em;
}
.ui-datepicker th {
padding: .7em .3em;
text-align: center;
font-weight: bold;
border: 0;
}
.ui-datepicker td {
border: 0;
padding: 1px;
}
.ui-datepicker td span,
.ui-datepicker td a {
display: block;
padding: .2em;
text-align: right;
text-decoration: none;
}
.ui-datepicker .ui-datepicker-buttonpane {
background-image: none;
margin: .7em 0 0 0;
padding: 0 .2em;
border-left: 0;
border-right: 0;
border-bottom: 0;
}
.ui-datepicker .ui-datepicker-buttonpane button {
float: right;
margin: .5em .2em .4em;
cursor: pointer;
padding: .2em .6em .3em .6em;
width: auto;
overflow: visible;
}
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
float: left;
}
/* with multiple calendars */
.ui-datepicker.ui-datepicker-multi {
width: auto;
}
.ui-datepicker-multi .ui-datepicker-group {
float: left;
}
.ui-datepicker-multi .ui-datepicker-group table {
width: 95%;
margin: 0 auto .4em;
}
.ui-datepicker-multi-2 .ui-datepicker-group {
width: 50%;
}
.ui-datepicker-multi-3 .ui-datepicker-group {
width: 33.3%;
}
.ui-datepicker-multi-4 .ui-datepicker-group {
width: 25%;
}
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
border-left-width: 0;
}
.ui-datepicker-multi .ui-datepicker-buttonpane {
clear: left;
}
.ui-datepicker-row-break {
clear: both;
width: 100%;
font-size: 0;
}
/* RTL support */
.ui-datepicker-rtl {
direction: rtl;
}
.ui-datepicker-rtl .ui-datepicker-prev {
right: 2px;
left: auto;
}
.ui-datepicker-rtl .ui-datepicker-next {
left: 2px;
right: auto;
}
.ui-datepicker-rtl .ui-datepicker-prev:hover {
right: 1px;
left: auto;
}
.ui-datepicker-rtl .ui-datepicker-next:hover {
left: 1px;
right: auto;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane {
clear: right;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane button {
float: left;
}
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
.ui-datepicker-rtl .ui-datepicker-group {
float: right;
}
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
border-right-width: 0;
border-left-width: 1px;
}
/* Icons */
.ui-datepicker .ui-icon {
display: block;
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
left: .5em;
top: .3em;
}
.ui-dialog {
position: absolute;
top: 0;
left: 0;
padding: .2em;
outline: 0;
}
.ui-dialog .ui-dialog-titlebar {
padding: .4em 1em;
position: relative;
}
.ui-dialog .ui-dialog-title {
float: left;
margin: .1em 0;
white-space: nowrap;
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
}
.ui-dialog .ui-dialog-titlebar-close {
position: absolute;
right: .3em;
top: 50%;
width: 20px;
margin: -10px 0 0 0;
padding: 1px;
height: 20px;
}
.ui-dialog .ui-dialog-content {
position: relative;
border: 0;
padding: .5em 1em;
background: none;
overflow: auto;
}
.ui-dialog .ui-dialog-buttonpane {
text-align: left;
border-width: 1px 0 0 0;
background-image: none;
margin-top: .5em;
padding: .3em 1em .5em .4em;
}
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
float: right;
}
.ui-dialog .ui-dialog-buttonpane button {
margin: .5em .4em .5em 0;
cursor: pointer;
}
.ui-dialog .ui-resizable-n {
height: 2px;
top: 0;
}
.ui-dialog .ui-resizable-e {
width: 2px;
right: 0;
}
.ui-dialog .ui-resizable-s {
height: 2px;
bottom: 0;
}
.ui-dialog .ui-resizable-w {
width: 2px;
left: 0;
}
.ui-dialog .ui-resizable-se,
.ui-dialog .ui-resizable-sw,
.ui-dialog .ui-resizable-ne,
.ui-dialog .ui-resizable-nw {
width: 7px;
height: 7px;
}
.ui-dialog .ui-resizable-se {
right: 0;
bottom: 0;
}
.ui-dialog .ui-resizable-sw {
left: 0;
bottom: 0;
}
.ui-dialog .ui-resizable-ne {
right: 0;
top: 0;
}
.ui-dialog .ui-resizable-nw {
left: 0;
top: 0;
}
.ui-draggable .ui-dialog-titlebar {
cursor: move;
}
.ui-draggable-handle {
-ms-touch-action: none;
touch-action: none;
}
.ui-resizable {
position: relative;
}
.ui-resizable-handle {
position: absolute;
font-size: 0.1px;
display: block;
-ms-touch-action: none;
touch-action: none;
}
.ui-resizable-disabled .ui-resizable-handle,
.ui-resizable-autohide .ui-resizable-handle {
display: none;
}
.ui-resizable-n {
cursor: n-resize;
height: 7px;
width: 100%;
top: -5px;
left: 0;
}
.ui-resizable-s {
cursor: s-resize;
height: 7px;
width: 100%;
bottom: -5px;
left: 0;
}
.ui-resizable-e {
cursor: e-resize;
width: 7px;
right: -5px;
top: 0;
height: 100%;
}
.ui-resizable-w {
cursor: w-resize;
width: 7px;
left: -5px;
top: 0;
height: 100%;
}
.ui-resizable-se {
cursor: se-resize;
width: 12px;
height: 12px;
right: 1px;
bottom: 1px;
}
.ui-resizable-sw {
cursor: sw-resize;
width: 9px;
height: 9px;
left: -5px;
bottom: -5px;
}
.ui-resizable-nw {
cursor: nw-resize;
width: 9px;
height: 9px;
left: -5px;
top: -5px;
}
.ui-resizable-ne {
cursor: ne-resize;
width: 9px;
height: 9px;
right: -5px;
top: -5px;
}
.ui-progressbar {
height: 2em;
text-align: left;
overflow: hidden;
}
.ui-progressbar .ui-progressbar-value {
margin: -1px;
height: 100%;
}
.ui-progressbar .ui-progressbar-overlay {
background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");
height: 100%;
filter: alpha(opacity=25); /* support: IE8 */
opacity: 0.25;
}
.ui-progressbar-indeterminate .ui-progressbar-value {
background-image: none;
}
.ui-selectable {
-ms-touch-action: none;
touch-action: none;
}
.ui-selectable-helper {
position: absolute;
z-index: 100;
border: 1px dotted black;
}
.ui-selectmenu-menu {
padding: 0;
margin: 0;
position: absolute;
top: 0;
left: 0;
display: none;
}
.ui-selectmenu-menu .ui-menu {
overflow: auto;
overflow-x: hidden;
padding-bottom: 1px;
}
.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup {
font-size: 1em;
font-weight: bold;
line-height: 1.5;
padding: 2px 0.4em;
margin: 0.5em 0 0 0;
height: auto;
border: 0;
}
.ui-selectmenu-open {
display: block;
}
.ui-selectmenu-text {
display: block;
margin-right: 20px;
overflow: hidden;
text-overflow: ellipsis;
}
.ui-selectmenu-button.ui-button {
text-align: left;
white-space: nowrap;
width: 14em;
}
.ui-selectmenu-icon.ui-icon {
float: right;
margin-top: 0;
}
.ui-slider {
position: relative;
text-align: left;
}
.ui-slider .ui-slider-handle {
position: absolute;
z-index: 2;
width: 1.2em;
height: 1.2em;
cursor: default;
-ms-touch-action: none;
touch-action: none;
}
.ui-slider .ui-slider-range {
position: absolute;
z-index: 1;
font-size: .7em;
display: block;
border: 0;
background-position: 0 0;
}
/* support: IE8 - See #6727 */
.ui-slider.ui-state-disabled .ui-slider-handle,
.ui-slider.ui-state-disabled .ui-slider-range {
filter: inherit;
}
.ui-slider-horizontal {
height: .8em;
}
.ui-slider-horizontal .ui-slider-handle {
top: -.3em;
margin-left: -.6em;
}
.ui-slider-horizontal .ui-slider-range {
top: 0;
height: 100%;
}
.ui-slider-horizontal .ui-slider-range-min {
left: 0;
}
.ui-slider-horizontal .ui-slider-range-max {
right: 0;
}
.ui-slider-vertical {
width: .8em;
height: 100px;
}
.ui-slider-vertical .ui-slider-handle {
left: -.3em;
margin-left: 0;
margin-bottom: -.6em;
}
.ui-slider-vertical .ui-slider-range {
left: 0;
width: 100%;
}
.ui-slider-vertical .ui-slider-range-min {
bottom: 0;
}
.ui-slider-vertical .ui-slider-range-max {
top: 0;
}
.ui-sortable-handle {
-ms-touch-action: none;
touch-action: none;
}
.ui-spinner {
position: relative;
display: inline-block;
overflow: hidden;
padding: 0;
vertical-align: middle;
}
.ui-spinner-input {
border: none;
background: none;
color: inherit;
padding: .222em 0;
margin: .2em 0;
vertical-align: middle;
margin-left: .4em;
margin-right: 2em;
}
.ui-spinner-button {
width: 1.6em;
height: 50%;
font-size: .5em;
padding: 0;
margin: 0;
text-align: center;
position: absolute;
cursor: default;
display: block;
overflow: hidden;
right: 0;
}
/* more specificity required here to override default borders */
.ui-spinner a.ui-spinner-button {
border-top-style: none;
border-bottom-style: none;
border-right-style: none;
}
.ui-spinner-up {
top: 0;
}
.ui-spinner-down {
bottom: 0;
}
.ui-tabs {
position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
padding: .2em;
}
.ui-tabs .ui-tabs-nav {
margin: 0;
padding: .2em .2em 0;
}
.ui-tabs .ui-tabs-nav li {
list-style: none;
float: left;
position: relative;
top: 0;
margin: 1px .2em 0 0;
border-bottom-width: 0;
padding: 0;
white-space: nowrap;
}
.ui-tabs .ui-tabs-nav .ui-tabs-anchor {
float: left;
padding: .5em 1em;
text-decoration: none;
}
.ui-tabs .ui-tabs-nav li.ui-tabs-active {
margin-bottom: -1px;
padding-bottom: 1px;
}
.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,
.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,
.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {
cursor: text;
}
.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {
cursor: pointer;
}
.ui-tabs .ui-tabs-panel {
display: block;
border-width: 0;
padding: 1em 1.4em;
background: none;
}
.ui-tooltip {
padding: 8px;
position: absolute;
z-index: 9999;
max-width: 300px;
}
body .ui-tooltip {
border-width: 2px;
}

File diff suppressed because one or more lines are too long

View File

@ -1,443 +0,0 @@
/*!
* jQuery UI CSS Framework 1.12.1
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/theming/
*
* To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6
*/
/* Component containers
----------------------------------*/
.ui-widget {
font-family: Arial,Helvetica,sans-serif;
font-size: 1em;
}
.ui-widget .ui-widget {
font-size: 1em;
}
.ui-widget input,
.ui-widget select,
.ui-widget textarea,
.ui-widget button {
font-family: Arial,Helvetica,sans-serif;
font-size: 1em;
}
.ui-widget.ui-widget-content {
border: 1px solid #c5c5c5;
}
.ui-widget-content {
border: 1px solid #dddddd;
background: #ffffff;
color: #333333;
}
.ui-widget-content a {
color: #333333;
}
.ui-widget-header {
border: 1px solid #dddddd;
background: #e9e9e9;
color: #333333;
font-weight: bold;
}
.ui-widget-header a {
color: #333333;
}
/* Interaction states
----------------------------------*/
.ui-state-default,
.ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default,
.ui-button,
/* We use html here because we need a greater specificity to make sure disabled
works properly when clicked or hovered */
html .ui-button.ui-state-disabled:hover,
html .ui-button.ui-state-disabled:active {
border: 1px solid #c5c5c5;
background: #f6f6f6;
font-weight: normal;
color: #454545;
}
.ui-state-default a,
.ui-state-default a:link,
.ui-state-default a:visited,
a.ui-button,
a:link.ui-button,
a:visited.ui-button,
.ui-button {
color: #454545;
text-decoration: none;
}
.ui-state-hover,
.ui-widget-content .ui-state-hover,
.ui-widget-header .ui-state-hover,
.ui-state-focus,
.ui-widget-content .ui-state-focus,
.ui-widget-header .ui-state-focus,
.ui-button:hover,
.ui-button:focus {
border: 1px solid #cccccc;
background: #ededed;
font-weight: normal;
color: #2b2b2b;
}
.ui-state-hover a,
.ui-state-hover a:hover,
.ui-state-hover a:link,
.ui-state-hover a:visited,
.ui-state-focus a,
.ui-state-focus a:hover,
.ui-state-focus a:link,
.ui-state-focus a:visited,
a.ui-button:hover,
a.ui-button:focus {
color: #2b2b2b;
text-decoration: none;
}
.ui-visual-focus {
box-shadow: 0 0 3px 1px rgb(94, 158, 214);
}
.ui-state-active,
.ui-widget-content .ui-state-active,
.ui-widget-header .ui-state-active,
a.ui-button:active,
.ui-button:active,
.ui-button.ui-state-active:hover {
border: 1px solid #003eff;
background: #007fff;
font-weight: normal;
color: #ffffff;
}
.ui-icon-background,
.ui-state-active .ui-icon-background {
border: #003eff;
background-color: #ffffff;
}
.ui-state-active a,
.ui-state-active a:link,
.ui-state-active a:visited {
color: #ffffff;
text-decoration: none;
}
/* Interaction Cues
----------------------------------*/
.ui-state-highlight,
.ui-widget-content .ui-state-highlight,
.ui-widget-header .ui-state-highlight {
border: 1px solid #dad55e;
background: #fffa90;
color: #777620;
}
.ui-state-checked {
border: 1px solid #dad55e;
background: #fffa90;
}
.ui-state-highlight a,
.ui-widget-content .ui-state-highlight a,
.ui-widget-header .ui-state-highlight a {
color: #777620;
}
.ui-state-error,
.ui-widget-content .ui-state-error,
.ui-widget-header .ui-state-error {
border: 1px solid #f1a899;
background: #fddfdf;
color: #5f3f3f;
}
.ui-state-error a,
.ui-widget-content .ui-state-error a,
.ui-widget-header .ui-state-error a {
color: #5f3f3f;
}
.ui-state-error-text,
.ui-widget-content .ui-state-error-text,
.ui-widget-header .ui-state-error-text {
color: #5f3f3f;
}
.ui-priority-primary,
.ui-widget-content .ui-priority-primary,
.ui-widget-header .ui-priority-primary {
font-weight: bold;
}
.ui-priority-secondary,
.ui-widget-content .ui-priority-secondary,
.ui-widget-header .ui-priority-secondary {
opacity: .7;
filter:Alpha(Opacity=70); /* support: IE8 */
font-weight: normal;
}
.ui-state-disabled,
.ui-widget-content .ui-state-disabled,
.ui-widget-header .ui-state-disabled {
opacity: .35;
filter:Alpha(Opacity=35); /* support: IE8 */
background-image: none;
}
.ui-state-disabled .ui-icon {
filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */
}
/* Icons
----------------------------------*/
/* states and images */
.ui-icon {
width: 16px;
height: 16px;
}
.ui-icon,
.ui-widget-content .ui-icon {
background-image: url("images/ui-icons_444444_256x240.png");
}
.ui-widget-header .ui-icon {
background-image: url("images/ui-icons_444444_256x240.png");
}
.ui-state-hover .ui-icon,
.ui-state-focus .ui-icon,
.ui-button:hover .ui-icon,
.ui-button:focus .ui-icon {
background-image: url("images/ui-icons_555555_256x240.png");
}
.ui-state-active .ui-icon,
.ui-button:active .ui-icon {
background-image: url("images/ui-icons_ffffff_256x240.png");
}
.ui-state-highlight .ui-icon,
.ui-button .ui-state-highlight.ui-icon {
background-image: url("images/ui-icons_777620_256x240.png");
}
.ui-state-error .ui-icon,
.ui-state-error-text .ui-icon {
background-image: url("images/ui-icons_cc0000_256x240.png");
}
.ui-button .ui-icon {
background-image: url("images/ui-icons_777777_256x240.png");
}
/* positioning */
.ui-icon-blank { background-position: 16px 16px; }
.ui-icon-caret-1-n { background-position: 0 0; }
.ui-icon-caret-1-ne { background-position: -16px 0; }
.ui-icon-caret-1-e { background-position: -32px 0; }
.ui-icon-caret-1-se { background-position: -48px 0; }
.ui-icon-caret-1-s { background-position: -65px 0; }
.ui-icon-caret-1-sw { background-position: -80px 0; }
.ui-icon-caret-1-w { background-position: -96px 0; }
.ui-icon-caret-1-nw { background-position: -112px 0; }
.ui-icon-caret-2-n-s { background-position: -128px 0; }
.ui-icon-caret-2-e-w { background-position: -144px 0; }
.ui-icon-triangle-1-n { background-position: 0 -16px; }
.ui-icon-triangle-1-ne { background-position: -16px -16px; }
.ui-icon-triangle-1-e { background-position: -32px -16px; }
.ui-icon-triangle-1-se { background-position: -48px -16px; }
.ui-icon-triangle-1-s { background-position: -65px -16px; }
.ui-icon-triangle-1-sw { background-position: -80px -16px; }
.ui-icon-triangle-1-w { background-position: -96px -16px; }
.ui-icon-triangle-1-nw { background-position: -112px -16px; }
.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
.ui-icon-arrow-1-n { background-position: 0 -32px; }
.ui-icon-arrow-1-ne { background-position: -16px -32px; }
.ui-icon-arrow-1-e { background-position: -32px -32px; }
.ui-icon-arrow-1-se { background-position: -48px -32px; }
.ui-icon-arrow-1-s { background-position: -65px -32px; }
.ui-icon-arrow-1-sw { background-position: -80px -32px; }
.ui-icon-arrow-1-w { background-position: -96px -32px; }
.ui-icon-arrow-1-nw { background-position: -112px -32px; }
.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
.ui-icon-arrowthick-1-n { background-position: 1px -48px; }
.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
.ui-icon-arrow-4 { background-position: 0 -80px; }
.ui-icon-arrow-4-diag { background-position: -16px -80px; }
.ui-icon-extlink { background-position: -32px -80px; }
.ui-icon-newwin { background-position: -48px -80px; }
.ui-icon-refresh { background-position: -64px -80px; }
.ui-icon-shuffle { background-position: -80px -80px; }
.ui-icon-transfer-e-w { background-position: -96px -80px; }
.ui-icon-transferthick-e-w { background-position: -112px -80px; }
.ui-icon-folder-collapsed { background-position: 0 -96px; }
.ui-icon-folder-open { background-position: -16px -96px; }
.ui-icon-document { background-position: -32px -96px; }
.ui-icon-document-b { background-position: -48px -96px; }
.ui-icon-note { background-position: -64px -96px; }
.ui-icon-mail-closed { background-position: -80px -96px; }
.ui-icon-mail-open { background-position: -96px -96px; }
.ui-icon-suitcase { background-position: -112px -96px; }
.ui-icon-comment { background-position: -128px -96px; }
.ui-icon-person { background-position: -144px -96px; }
.ui-icon-print { background-position: -160px -96px; }
.ui-icon-trash { background-position: -176px -96px; }
.ui-icon-locked { background-position: -192px -96px; }
.ui-icon-unlocked { background-position: -208px -96px; }
.ui-icon-bookmark { background-position: -224px -96px; }
.ui-icon-tag { background-position: -240px -96px; }
.ui-icon-home { background-position: 0 -112px; }
.ui-icon-flag { background-position: -16px -112px; }
.ui-icon-calendar { background-position: -32px -112px; }
.ui-icon-cart { background-position: -48px -112px; }
.ui-icon-pencil { background-position: -64px -112px; }
.ui-icon-clock { background-position: -80px -112px; }
.ui-icon-disk { background-position: -96px -112px; }
.ui-icon-calculator { background-position: -112px -112px; }
.ui-icon-zoomin { background-position: -128px -112px; }
.ui-icon-zoomout { background-position: -144px -112px; }
.ui-icon-search { background-position: -160px -112px; }
.ui-icon-wrench { background-position: -176px -112px; }
.ui-icon-gear { background-position: -192px -112px; }
.ui-icon-heart { background-position: -208px -112px; }
.ui-icon-star { background-position: -224px -112px; }
.ui-icon-link { background-position: -240px -112px; }
.ui-icon-cancel { background-position: 0 -128px; }
.ui-icon-plus { background-position: -16px -128px; }
.ui-icon-plusthick { background-position: -32px -128px; }
.ui-icon-minus { background-position: -48px -128px; }
.ui-icon-minusthick { background-position: -64px -128px; }
.ui-icon-close { background-position: -80px -128px; }
.ui-icon-closethick { background-position: -96px -128px; }
.ui-icon-key { background-position: -112px -128px; }
.ui-icon-lightbulb { background-position: -128px -128px; }
.ui-icon-scissors { background-position: -144px -128px; }
.ui-icon-clipboard { background-position: -160px -128px; }
.ui-icon-copy { background-position: -176px -128px; }
.ui-icon-contact { background-position: -192px -128px; }
.ui-icon-image { background-position: -208px -128px; }
.ui-icon-video { background-position: -224px -128px; }
.ui-icon-script { background-position: -240px -128px; }
.ui-icon-alert { background-position: 0 -144px; }
.ui-icon-info { background-position: -16px -144px; }
.ui-icon-notice { background-position: -32px -144px; }
.ui-icon-help { background-position: -48px -144px; }
.ui-icon-check { background-position: -64px -144px; }
.ui-icon-bullet { background-position: -80px -144px; }
.ui-icon-radio-on { background-position: -96px -144px; }
.ui-icon-radio-off { background-position: -112px -144px; }
.ui-icon-pin-w { background-position: -128px -144px; }
.ui-icon-pin-s { background-position: -144px -144px; }
.ui-icon-play { background-position: 0 -160px; }
.ui-icon-pause { background-position: -16px -160px; }
.ui-icon-seek-next { background-position: -32px -160px; }
.ui-icon-seek-prev { background-position: -48px -160px; }
.ui-icon-seek-end { background-position: -64px -160px; }
.ui-icon-seek-start { background-position: -80px -160px; }
/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
.ui-icon-seek-first { background-position: -80px -160px; }
.ui-icon-stop { background-position: -96px -160px; }
.ui-icon-eject { background-position: -112px -160px; }
.ui-icon-volume-off { background-position: -128px -160px; }
.ui-icon-volume-on { background-position: -144px -160px; }
.ui-icon-power { background-position: 0 -176px; }
.ui-icon-signal-diag { background-position: -16px -176px; }
.ui-icon-signal { background-position: -32px -176px; }
.ui-icon-battery-0 { background-position: -48px -176px; }
.ui-icon-battery-1 { background-position: -64px -176px; }
.ui-icon-battery-2 { background-position: -80px -176px; }
.ui-icon-battery-3 { background-position: -96px -176px; }
.ui-icon-circle-plus { background-position: 0 -192px; }
.ui-icon-circle-minus { background-position: -16px -192px; }
.ui-icon-circle-close { background-position: -32px -192px; }
.ui-icon-circle-triangle-e { background-position: -48px -192px; }
.ui-icon-circle-triangle-s { background-position: -64px -192px; }
.ui-icon-circle-triangle-w { background-position: -80px -192px; }
.ui-icon-circle-triangle-n { background-position: -96px -192px; }
.ui-icon-circle-arrow-e { background-position: -112px -192px; }
.ui-icon-circle-arrow-s { background-position: -128px -192px; }
.ui-icon-circle-arrow-w { background-position: -144px -192px; }
.ui-icon-circle-arrow-n { background-position: -160px -192px; }
.ui-icon-circle-zoomin { background-position: -176px -192px; }
.ui-icon-circle-zoomout { background-position: -192px -192px; }
.ui-icon-circle-check { background-position: -208px -192px; }
.ui-icon-circlesmall-plus { background-position: 0 -208px; }
.ui-icon-circlesmall-minus { background-position: -16px -208px; }
.ui-icon-circlesmall-close { background-position: -32px -208px; }
.ui-icon-squaresmall-plus { background-position: -48px -208px; }
.ui-icon-squaresmall-minus { background-position: -64px -208px; }
.ui-icon-squaresmall-close { background-position: -80px -208px; }
.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
/* Misc visuals
----------------------------------*/
/* Corner radius */
.ui-corner-all,
.ui-corner-top,
.ui-corner-left,
.ui-corner-tl {
border-top-left-radius: 3px;
}
.ui-corner-all,
.ui-corner-top,
.ui-corner-right,
.ui-corner-tr {
border-top-right-radius: 3px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-left,
.ui-corner-bl {
border-bottom-left-radius: 3px;
}
.ui-corner-all,
.ui-corner-bottom,
.ui-corner-right,
.ui-corner-br {
border-bottom-right-radius: 3px;
}
/* Overlays */
.ui-widget-overlay {
background: #aaaaaa;
opacity: .003;
filter: Alpha(Opacity=.3); /* support: IE8 */
}
.ui-widget-shadow {
-webkit-box-shadow: 0px 0px 5px #666666;
box-shadow: 0px 0px 5px #666666;
}

File diff suppressed because one or more lines are too long

View File

@ -1,74 +0,0 @@
{
"name": "jquery-ui",
"title": "jQuery UI",
"description": "A curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library.",
"version": "1.12.1",
"homepage": "http://jqueryui.com",
"author": {
"name": "jQuery Foundation and other contributors",
"url": "https://github.com/jquery/jquery-ui/blob/1.12.1/AUTHORS.txt"
},
"main": "ui/widget.js",
"maintainers": [
{
"name": "Scott González",
"email": "scott.gonzalez@gmail.com",
"url": "http://scottgonzalez.com"
},
{
"name": "Jörn Zaefferer",
"email": "joern.zaefferer@gmail.com",
"url": "http://bassistance.de"
},
{
"name": "Mike Sherov",
"email": "mike.sherov@gmail.com",
"url": "http://mike.sherov.com"
},
{
"name": "TJ VanToll",
"email": "tj.vantoll@gmail.com",
"url": "http://tjvantoll.com"
},
{
"name": "Felix Nagel",
"email": "info@felixnagel.com",
"url": "http://www.felixnagel.com"
},
{
"name": "Alex Schmitz",
"email": "arschmitz@gmail.com",
"url": "https://github.com/arschmitz"
}
],
"repository": {
"type": "git",
"url": "git://github.com/jquery/jquery-ui.git"
},
"bugs": "https://bugs.jqueryui.com/",
"license": "MIT",
"scripts": {
"test": "grunt"
},
"dependencies": {},
"devDependencies": {
"commitplease": "2.3.0",
"grunt": "0.4.5",
"grunt-bowercopy": "1.2.4",
"grunt-cli": "0.1.13",
"grunt-compare-size": "0.4.0",
"grunt-contrib-concat": "0.5.1",
"grunt-contrib-csslint": "0.5.0",
"grunt-contrib-jshint": "0.12.0",
"grunt-contrib-qunit": "1.0.1",
"grunt-contrib-requirejs": "0.4.4",
"grunt-contrib-uglify": "0.11.1",
"grunt-git-authors": "3.1.0",
"grunt-html": "6.0.0",
"grunt-jscs": "2.1.0",
"load-grunt-tasks": "3.4.0",
"rimraf": "2.5.1",
"testswarm": "1.1.0"
},
"keywords": []
}

View File

@ -1,39 +0,0 @@
function enableButton() {
var enablebutton = '<form action="/enable" method="POST"> <button type="submit">Enable</button> </form> ';
var disablebutton = '<form action="/disable" method="POST"> <button style="background-color: red;" type="submit">Disable</button> </form> ';
var enabled = getCookie('enabled');
if (enabled == "True") {
return disablebutton;
} else {
return enablebutton;
}
}
function getCookie(cname) {
var name = cname + '=';
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function listformat(list) {
list = list.replace(/\"/g, '')
list = list.split('\\012');
return list.join('\n');
}
document.getElementById("enablebutton").innerHTML = enableButton();
document.getElementById("goodlist").innerHTML = listformat(getCookie("goodlist"));
document.getElementById("blocklist").innerHTML = listformat(getCookie("blocklist"));

File diff suppressed because one or more lines are too long

View File

@ -1,38 +0,0 @@
<head>
<title>Ticketfrei</title>
<link rel='stylesheet' href='css/style.css'>
<meta name='og:title' content='Register - Ticketfrei'/>
<meta name='og:description' content='A bot against control society! Nobody should have to pay for public transport. Run it in your city!'/>
<meta name='og:image' content="https://ticketfrei.links-tech.org/static/img/ticketfrei-og-image.png"/>
<meta name='og:image:alt' content='Ticketfrei'/>
<meta name='og:type' content='website' />
</head>
<body>
<div class="area">
<h1><a href="/"><img src="/static/img/ticketfrei_logo.png" alt="Ticketfrei" height="150px" align="center" style="float: none;"></a></h1>
<form action="../register" method="post">
<div class="container">
<label><b>Email</b></label>
<input type="text" placeholder="Enter Email" name="email" required>
<label><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="psw" required>
<label><b>Repeat Password</b></label>
<input type="password" placeholder="Repeat Password" name="psw-repeat" required>
<p>By creating an account you agree to our <a href="#">Terms & Privacy</a>.</p>
<div class="clearfix">
<button type="submit" class="signupbtn">Sign Up</button>
</div>
</div>
</form>
<div class=footer>
Contribute on <a href="https://github.com/b3yond/ticketfrei">GitHub!</a>
</div>
</div>
</body>

View File

@ -1,17 +0,0 @@
% rebase('template/wrapper.tpl')
<%
import markdown as md
html = md.markdown(markdown)
%>
% if info is not None:
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>{{!info}}</p>
</div>
</div>
% end
{{!html}}

View File

@ -1,9 +0,0 @@
<form action="login" method="POST">
<label for="email">Email</label>
<input type="text" placeholder="Enter Email" name="email" id="email" required>
<label for="pass">Password</label>
<input type="password" placeholder="Enter Password" name="pass" id="pass" required>
<button type="submit">Login</button>
</form>

View File

@ -1,2 +0,0 @@
% rebase('template/wrapper.tpl', title='Login')
% include('template/login-plain.tpl')

View File

@ -1,16 +0,0 @@
% rebase('template/wrapper.tpl')
<%
import markdown as md
html = md.markdown(mail_md)
%>
{{!html}}
<form action="/city/mail/submit/{{!city}}" method="post">
<input type="text" name="mailaddress" placeholder="E-Mail address" id="mailaddress">
<input name='confirm' value='Subscribe to E-Mail notifications' type='submit'/>
</form>
<br>
<p style="text-align: center;"><a href="/city/{{!city}}">Back to Ticketfrei {{!city}} overview</a></p>

View File

@ -1,71 +0,0 @@
% rebase('template/wrapper.tpl')
% if defined('info'):
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>{{!info}}</p>
</div>
</div>
<br>
% end
% include('template/login-plain.tpl')
<h1>Features</h1>
<p>
Don't pay for public transport. Instead, warn each other
from ticket controllers! With Ticketfrei, you can turn
your city into a paradise for fare dodgers.
</p>
<p>
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 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>
<h2>How to get Ticketfrei to my city?</h2>
<p>
We try to make it as easy as possible to spread Ticketfrei
to other citys. There are four basic steps:
</p>
<ul>
<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 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 yet
the case. Ticketfrei's approach is to enable people to
reclaim public transportation.
</p>
<p>
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.
</p>
<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 could look
like!
</p>

View File

@ -1,16 +0,0 @@
<form action="register" method="post">
<label for="email">Email</label>
<input type="text" placeholder="Enter Email" name="email" id="email" required>
<label for="city">City</label>
<input type='text' name='city' placeholder='Barcelona'/>
<label for="pass">Password</label>
<input type="password" placeholder="Enter Password" name="pass" id="pass" required>
<label for="pass-repeat">Repeat Password</label>
<input type="password" placeholder="Repeat Password" name="pass-repeat" id="pass-repeat" required>
<button type="submit">Sign Up</button>
</form>

View File

@ -1,10 +0,0 @@
% rebase('template/wrapper.tpl', title='Register')
% if defined('info'):
<div class="ui-widget">
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
<p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>{{!info}}</p>
</div>
</div>
% else:
% include('template/register-plain.tpl')
% end

View File

@ -1,164 +0,0 @@
% rebase('template/wrapper.tpl')
<a href="/logout/"><button>Logout</button></a>
% if enabled:
<div id="enablebutton" style="float: right; padding: 2em;">Disable</div>
% else:
<div id="enablebutton" style="float: right; padding: 2em;" color="red">Enable</div>
% end
<a class='button' style="padding: 1.5em;" href="/login/twitter">
<picture>
<source type='image/webp' sizes='20px' srcset="/static-cb/1517673283/twitter-20.webp 20w,/static-cb/1517673283/twitter-40.webp 40w,/static-cb/1517673283/twitter-80.webp 80w,"/>
<source type='image/png' sizes='20px' srcset="/static-cb/1517673283/twitter-20.png 20w,/static-cb/1517673283/twitter-40.png 40w,/static-cb/1517673283/twitter-80.png 80w,"/>
<img src="https://patriciaannbridewell.files.wordpress.com/2014/04/official-twitter-logo-tile.png" alt="" />
</picture>
Log in with Twitter
</a>
<section>
<h2>Log in with Mastodon</h2>
<form action="/login/mastodon" method='post'>
<label for="email">E-Mail of your Mastodon-Account</label>
<input type="text" placeholder="Enter Email" name="email" id="email" required>
<label for="pass">Mastodon Password</label>
<input type="password" placeholder="Enter Password" name="pass" id="pass" required>
<label>Mastodon instance:
<input type='text' name='instance_url' list='instances' placeholder='social.example.net'/>
</label>
<datalist id='instances'>
<option value=''>
<option value='anticapitalist.party'>
<option value='awoo.space'>
<option value='cybre.space'>
<option value='mastodon.social'>
<option value='glitch.social'>
<option value='botsin.space'>
<option value='witches.town'>
<option value='social.wxcafe.net'>
<option value='monsterpit.net'>
<option value='mastodon.xyz'>
<option value='a.weirder.earth'>
<option value='chitter.xyz'>
<option value='sins.center'>
<option value='dev.glitch.social'>
<option value='computerfairi.es'>
<option value='niu.moe'>
<option value='icosahedron.website'>
<option value='hostux.social'>
<option value='hyenas.space'>
<option value='instance.business'>
<option value='mastodon.sdf.org'>
<option value='pawoo.net'>
<option value='pouet.it'>
<option value='scalie.business'>
<option value='sleeping.town'>
<option value='social.koyu.space'>
<option value='sunshinegardens.org'>
<option value='vcity.network'>
<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>
<%
# todo: hide this part, if there is already a telegram bot connected.
%>
<div>
<h2>Connect with Telegram</h2>
<p>
If you have a Telegram account, you can register a bot there. Just
write to @botfather. There are detailed instructions on
<a href="https://botsfortelegram.com/project/the-bot-father/" target="_blank">
Bots for Telegram</a>.
</p>
<p>
The botfather will give you an API key - with the API key, Ticketfrei
can use the Telegram bot. Enter it here:
</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>
<div>
<h2>Edit your city page</h2>
<p>
With your bot, we generated you a page, which you can use for promotion:
<a href="/city/{{city}}" target="_blank">Ticketfrei {{city}}</a> You
can change what your users will read there, and adjust it to your
needs.
</p>
<p>
<b>You should definitely adjust the Social Media, E-Mail, and Telegram
profile links.</b>
Also consider adding this link to the text: <a href="/city/mail/{{city}}"
target="_blank">Link to the mail subscription page</a>. Your readers
can use this to subscribe to mail notifications.
</p>
<p>
So this is the default text we suggest:
</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>
<div>
<h2>Edit your mail subscription page</h2>
<p>
There is also a page where users can subscribe to mail notifications:
<a href="/city/mail/{{city}}" target="_blank">Ticketfrei {{city}}</a>.
You can change what your users will read there, and adjust it to your
needs.
</p>
<p>
So this is the default text we suggest:
</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>
<div>
<h2>Edit your trigger patterns</h2>
<p>
These words have to be contained in a report. If none of these
expressions is in the report, it will be ignored by the bot. You can
use the defaults, or enter some expressions specific to your city and
language.
</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>
<div>
<h2>Edit the blocklist</h2>
<p>
These words are not allowed in reports. If you encounter spam, you can
add more here - the bot will ignore reports which use such words.
There are words which you can't exclude from the blocklist, e.g.
certain racist, sexist, or antisemitic slurs.
</p>
<form action="/settings/blocklist" method="post">
<textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical">{{badwords}}</textarea>
<input name='csrf' value='{{csrf}}' type='hidden' />
<input name='confirm' value='Submit' type='submit'/>
</form>
</div>

View File

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Ticketfrei - {{get('title', 'A bot against control society!')}}</title>
<meta name='og:title' content='Ticketfrei'/>
<meta name='og:description' content='A bot against control society! Nobody should have to pay for public transport. Find out where ticket controllers are!'/>
<meta name='og:image' content="https://ticketfrei.links-tech.org/static/img/ticketfrei-og-image.jpg"/>
<meta name='og:image:alt' content='Ticketfrei'/>
<meta name='og:type' content='website' />
<link rel='stylesheet' href='/static/css/style.css'>
<link rel="stylesheet" href="/static/jquery-ui-1.12.1/jquery-ui.min.css">
<script src="/static/js/jquery-3.3.1.min.js"></script>
<script src="/static/jquery-ui-1.12.1/jquery-ui.min.js"></script>
</head>
<body>
<div id="content">
<a href="/"><img src="/static/img/ticketfrei_logo.png" alt="<h1>Ticketfrei</h1>" id="logo"></a>
{{get('title', '')}}
% if defined('error'):
<div class="ui-widget">
<div class="ui-state-error ui-corner-all" style="padding: 0.7em;">
<p><span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>{{error}}</p>
</div>
</div>
% end
{{!base}}
<p>Contribute on <a href="https://github.com/b3yond/ticketfrei">GitHub!</a></p>
</div>
</body>
</html>

61
ticketfrei.py Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
import logging
import pytoml as toml
import time
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('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)
bots = []
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:
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!")
except:
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)

75
trigger.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
import pytoml as toml
import os
import re
class Trigger(object):
"""
This class provides a filter to test a string against.
"""
def __init__(self, config):
self.config = config
try:
goodlistpath = config['trigger']['goodlist_path']
except KeyError:
goodlistpath = 'goodlists'
# load goodlists
self.goodlist = []
for filename in os.listdir(goodlistpath):
with open(os.path.join(goodlistpath, filename), "r+") as listfile:
for pattern in listfile:
pattern = pattern.strip()
if pattern:
self.goodlist.append(re.compile(pattern, re.IGNORECASE))
try:
blacklistpath = config['trigger']['blacklist_path']
except KeyError:
blacklistpath = 'blacklists'
# load blacklists
self.blacklist = set()
for filename in os.listdir(blacklistpath):
with open(os.path.join(blacklistpath, filename), "r+") as listfile:
for word in listfile:
word = word.strip()
if word:
self.blacklist.add(word)
def is_ok(self, message):
"""
checks if a string contains no bad words and at least 1 good word.
:param message: A given string. Tweet or Toot, cleaned from html.
:return: If the string passes the test
"""
for pattern in self.goodlist:
if pattern.search(message) is not None:
break
else:
# no pattern matched
return False
for word in message.lower().split():
if word in self.blacklist:
return False
return True
if __name__ == "__main__":
with open("config.toml", "r") as configfile:
config = toml.load(configfile)
print("testing the trigger")
trigger = Trigger(config)
print("Printing words which trigger the bot:")
for i in trigger.goodlist:
print(i)
print()
print("Printing words which block a bot:")
for i in trigger.blacklist:
print(i)

476
user.py
View File

@ -1,476 +0,0 @@
from config import config
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
import logging
logger = logging.getLogger("main")
class User(object):
def __init__(self, uid):
# set cookie
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,))
passhash, = db.cur.fetchone()
return scrypt_mcf_check(passhash.encode('ascii'),
password.encode('utf-8'))
def password(self, password):
passhash = scrypt_mcf(password.encode('utf-8')).decode('ascii')
db.execute("UPDATE user SET passhash=? WHERE id=?;",
(passhash, self.uid))
db.commit()
password = property(None, password) # setter only, can't read back
@property
def enabled(self):
db.execute("SELECT enabled FROM user WHERE id=?;", (self.uid, ))
return bool(db.cur.fetchone()[0])
@enabled.setter
def enabled(self, enabled):
db.execute("UPDATE user SET enabled=? WHERE id=?",
(1 if enabled else 0, self.uid))
db.commit()
@property
def emails(self):
db.execute("SELECT email FROM email WHERE user_id=?;", (self.uid,))
return (*db.cur.fetchall(),)
def delete_email(self, email):
db.execute("SELECT COUNT(*) FROM email WHERE user_id=?", (self.uid,))
if db.cur.fetchone()[0] == 1:
return False # don't allow to delete last email
db.execute("DELETE FROM email WHERE user_id=? AND email=?;",
(self.uid, email))
db.commit()
return True
def email_token(self, email):
return jwt.encode({
'email': email,
'uid': self.uid
}, db.get_secret()).decode('ascii')
def is_appropriate(self, report):
db.execute("SELECT patterns FROM triggerpatterns WHERE user_id=?;",
(self.uid, ))
patterns = db.cur.fetchone()[0]
for pattern in patterns.splitlines():
if pattern.lower() in report.text.lower():
break
else:
# no pattern matched
logger.error("Message didn't trigger goodlist: " + report.text)
return False
default_badwords = """
bitch
whore
hitler
slut
hure
jude
schwuchtel
schlampe
fag
faggot
nigger
neger
schlitz
"""
db.execute("SELECT words FROM badwords WHERE user_id=?;",
(self.uid, ))
badwords = db.cur.fetchone()
for word in report.text.lower().split():
if word in badwords[0].splitlines():
logger.error("Word " + word + " triggered the spam filter on message: " + report.text)
return False
for word in report.text.lower().split():
if word in default_badwords.splitlines():
logger.error("Word " + word + " triggered the spam filter on message: " + report.text)
return False
logger.info("Valid report: " + report.text + " | username: " + report.author)
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
WHERE user_id = ? AND active = 1;""",
(self.uid,))
row = db.cur.fetchone()
return row[0]
def get_telegram_subscribers(self):
db.execute("""SELECT subscriber_id
FROM telegram_subscribers
WHERE user_id = ?;""",
(self.uid,))
rows = db.cur.fetchall()
return rows
def add_telegram_subscribers(self, subscriber_id):
db.execute("""INSERT INTO telegram_subscribers (
user_id, subscriber_id) VALUES(?, ?);""",
(self.uid, subscriber_id))
db.commit()
def remove_telegram_subscribers(self, subscriber_id):
db.execute("""DELETE
FROM telegram_subscribers
WHERE user_id = ?
AND subscriber_id = ?;""",
(self.uid, subscriber_id))
db.commit()
def get_masto_credentials(self):
db.execute("""SELECT access_token, instance_id
FROM mastodon_accounts
WHERE user_id = ? AND active = 1;""",
(self.uid,))
row = db.cur.fetchone()
db.execute("""SELECT instance, client_id, client_secret
FROM mastodon_instances
WHERE id = ?;""",
(row[1],))
instance = db.cur.fetchone()
return instance[1], instance[2], row[0], instance[0]
def toot_is_seen(self, toot_uri):
db.execute("SELECT COUNT(*) FROM seen_toots WHERE user_id = ? AND toot_uri = ?;",
(self.uid, toot_uri))
return db.cur.fetchone()[0] > 0
def toot_witness(self, toot_uri):
db.execute("INSERT INTO seen_toots (toot_uri, user_id) VALUES (?,?);",
(toot_uri, self.uid))
db.commit()
def get_seen_tweet(self):
db.execute("SELECT tweet_id FROM seen_tweets WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0]
def save_seen_tweet(self, tweet_id):
if tweet_id > self.get_seen_tweet():
db.execute("UPDATE seen_tweets SET tweet_id = ? WHERE user_id = ?;",
(tweet_id, self.uid))
db.commit()
def get_seen_dm(self):
db.execute("SELECT message_id FROM seen_dms WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()
def save_seen_dm(self, tweet_id):
db.execute("UPDATE seen_dms SET message_id = ? WHERE user_id = ?;",
(tweet_id, self.uid))
db.commit()
def get_seen_tg(self):
db.execute("SELECT tg_id FROM seen_telegrams WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0]
def save_seen_tg(self, tg_id):
db.execute("UPDATE seen_telegrams SET tg_id = ? WHERE user_id = ?;",
(tg_id, self.uid))
db.commit()
def get_mailinglist(self):
db.execute("SELECT email FROM mailinglist WHERE user_id = ?;", (self.uid, ))
return db.cur.fetchall()
def get_seen_mail(self):
db.execute("SELECT mail_date FROM seen_mail WHERE user_id = ?;", (self.uid, ))
return db.cur.fetchone()[0]
def save_seen_mail(self, mail_date):
db.execute("UPDATE seen_mail SET mail_date = ? WHERE user_id = ?;",
(mail_date, self.uid))
db.commit()
def set_trigger_words(self, patterns):
db.execute("UPDATE triggerpatterns SET patterns = ? WHERE user_id = ?;",
(patterns, self.uid))
db.commit()
def get_trigger_words(self):
db.execute("SELECT patterns FROM triggerpatterns WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0]
def add_subscriber(self, email):
db.execute("INSERT INTO mailinglist(user_id, email) VALUES(?, ?);", (self.uid, email))
db.commit()
def remove_subscriber(self, email):
db.execute("DELETE FROM mailinglist WHERE email = ? AND user_id = ?;", (email, self.uid))
db.commit()
def set_badwords(self, words):
db.execute("UPDATE badwords SET words = ? WHERE user_id = ?;",
(words, self.uid))
db.commit()
def get_badwords(self):
db.execute("SELECT words FROM badwords WHERE user_id = ?;",
(self.uid,))
return db.cur.fetchone()[0]
def state(self):
# necessary:
# - city
# - markdown
# - mail_md
# - goodlist
# - blocklist
# - csrf
# - logged in with twitter?
# - logged in with mastodon?
# - enabled?
citydict = db.user_facing_properties(self.get_city())
return dict(city=citydict['city'],
markdown=citydict['markdown'],
mail_md=citydict['mail_md'],
triggerwords=self.get_trigger_words(),
badwords=self.get_badwords(),
enabled=self.enabled,
csrf=self.get_csrf())
def save_request_token(self, token):
db.execute("""INSERT INTO
twitter_request_tokens(
user_id, request_token, request_token_secret
) VALUES(?, ?, ?);""",
(self.uid, token["oauth_token"],
token["oauth_token_secret"]))
db.commit()
def get_request_token(self):
db.execute("""SELECT request_token, request_token_secret
FROM twitter_request_tokens
WHERE user_id = ?;""", (self.uid,))
request_token = db.cur.fetchone()
db.execute("""DELETE FROM twitter_request_tokens
WHERE user_id = ?;""", (self.uid,))
db.commit()
return {"oauth_token": request_token[0],
"oauth_token_secret": request_token[1]}
def save_twitter_token(self, access_token, access_token_secret):
db.execute("""INSERT INTO twitter_accounts(
user_id, client_id, client_secret
) VALUES(?, ?, ?);""",
(self.uid, access_token, access_token_secret))
db.execute("""INSERT INTO seen_tweets(user_id, tweet_id) VALUES (?, ?);""",
(self.uid, 0))
db.commit()
def get_twitter_token(self):
db.execute("SELECT client_id, client_secret FROM twitter_accounts WHERE user_id = ?;",
(self.uid, ))
return db.cur.fetchone()
def get_twitter_credentials(self):
keys = [config['twitter']['consumer_key'],
config['twitter']['consumer_secret']]
row = self.get_twitter_token()
keys.append(row[0])
keys.append(row[1])
return keys
def update_telegram_key(self, apikey):
db.execute("UPDATE telegram_accounts SET apikey = ? WHERE user_id = ?;", (apikey, self.uid))
db.commit()
def get_mastodon_app_keys(self, instance):
db.execute("""SELECT client_id, client_secret
FROM mastodon_instances
WHERE instance = ?;""", (instance,))
try:
row = db.cur.fetchone()
client_id = row[0]
client_secret = row[1]
return client_id, client_secret
except TypeError:
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(
instance, client_id, client_secret
) VALUES(?, ?, ?);""",
(instance, client_id, client_secret))
db.commit()
return client_id, client_secret
def save_masto_token(self, access_token, instance):
db.execute("""SELECT id
FROM mastodon_instances
WHERE instance = ?;""", (instance,))
instance_id = db.cur.fetchone()[0]
db.execute("INSERT INTO mastodon_accounts(user_id, access_token, instance_id, active) "
"VALUES(?, ?, ?, ?);", (self.uid, access_token, instance_id, 1))
db.commit()
def set_markdown(self, markdown):
db.execute("UPDATE cities SET markdown = ? WHERE user_id = ?;",
(markdown, self.uid))
db.commit()
def set_mail_md(self, mail_md):
db.execute("UPDATE cities SET mail_md = ? WHERE user_id = ?;",
(mail_md, self.uid))
db.commit()
def get_city(self):
db.execute("SELECT city FROM cities WHERE user_id == ?;", (self.uid, ))
return db.cur.fetchone()[0]
def set_city(self, city):
masto_link = "https://example.mastodon.social/@" + city # get masto_link
twit_link = "https://example.twitter.com/" + city # get twit_link
mailinglist = city + "@" + config['web']['host']
markdown = """# Wie funktioniert Ticketfrei?
Willst du mithelfen, Ticketkontrolleur\*innen zu überwachen?
Willst du einen Fahrscheinfreien ÖPNV erkämpfen?
## Ist es gerade sicher, schwarz zu fahren?
Schau einfach auf das Profil unseres Bots: """ + twit_link + """
Hat jemand vor kurzem etwas über Kontrolleur\*innen gepostet?
* Wenn ja, dann kauf dir vllt lieber ein Ticket. In Nürnberg
haben wir die Erfahrung gemacht, dass Kontis normalerweile
ungefähr ne Woche aktiv sind, ein paar Stunden am Tag. Wenn es
also in den letzten Stunden einen Bericht gab, pass lieber
auf.
* Wenn nicht, ist es wahrscheinlich kein Problem :)
Wir können natürlich nicht garantieren, dass es sicher ist,
also pass trotzdem auf, wer auf dem Bahnsteig steht.
Aber je mehr Leute mitmachen, desto eher kannst du dir sicher
sein, dass wir sie finden, bevor sie uns finden.
Wenn du immer direkt gewarnt werden willst, kannst du auch die
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:
## Was mache ich, wenn ich Kontis sehe?
Ganz einfach, du schreibst es den anderen. Das geht entweder
* mit Mastodon [Link zu unserem Profil](""" + masto_link + """)
* über Twitter: [Link zu unserem Profil](""" + twit_link + """)
* über Telegram an [@ticketfrei_""" + city + "_bot](https://t.me/ticketfrei_" \
+ city + """_bot)
* Oder per Mail an [""" + mailinglist + "](mailto:" + mailinglist + """), wenn
ihr kein Social Media benutzen wollt.
Schreibe einfach einen Toot oder einen Tweet, der den Bot
mentioned, und gib an
* Wo du die Kontis gesehen hast
* Welche Linie sie benutzen und in welche Richtung sie fahren.
Zum Beispiel so:
![Screenshot of writing a Toot](https://github.com/b3yond/ticketfrei/raw/stable1/guides/tooting_screenshot.png)
![A toot ready to be shared](https://github.com/b3yond/ticketfrei/raw/stable1/guides/toot_screenshot.png)
Der Bot wird die Nachricht dann weiterverbreiten, auch zu den
anderen Netzwerken.
Dann können andere Leute das lesen und sicher vor Kontis sein.
Danke, dass du mithilfst, öffentlichen Verkehr für alle
sicherzustellen!
## Kann ich darauf vertrauen, was random stranger from the Internet mir da erzählen?
Aber natürlich! Wir haben Katzenbilder!
![Katzenbilder!](https://lorempixel.com/550/300/cats)
Glaubt besser nicht, wenn jemand postet, dass die Luft da und
da gerade rein ist.
Das ist vielleicht sogar gut gemeint - aber klar könnte die
VAG sich hinsetzen und einfach lauter Falschmeldungen posten.
Aber Falschmeldungen darüber, dass gerade Kontis i-wo unterwegs
sind?
Das macht keinen Sinn.
Im schlimmsten Fall kauft jmd mal eine Fahrkarte mehr - aber
kann sonst immer schwarz fahren.
Also ja - es macht Sinn, uns zu vertrauen, wenn wir sagen, wo
gerade Kontis sind.
## Was ist Mastodon und warum sollte ich es benutzen?
Mastodon ist ein dezentrales soziales Netzwerk - so wie
Twitter, nur ohne Monopol und Zentralismus.
Ihr könnt Kurznachrichten (Toots) über alles mögliche
schreiben, und euch mit anderen austauschen.
Mastodon ist Open Source, Privatsphäre-freundlich und relativ
sicher vor Zensur.
Um Mastodon zu benutzen, besucht diese Seite:
[https://joinmastodon.org/](https://joinmastodon.org/)
"""
mail_md = """# Immer up-to-date
Du bist viel unterwegs und hast keine Lust, jedes Mal auf das Profil des Bots
zu schauen? Kein Problem. Unsere Mail Notifications benachrichtigen dich, wenn
irgendwo Kontis gesehen werden.
Wenn du uns deine E-Mail-Adresse gibst, kriegst du bei jedem Konti-Report eine
Mail. Wenn du eine Mail-App auf dem Handy hast, so wie
[K9Mail](https://k9mail.github.io/), kriegst du sogar eine Push Notification. So
bist du immer Up-to-date über alles, was im Verkehrsnetz passiert.
## Keine Sorge
Wir benutzen deine E-Mail-Adresse selbstverständlich für nichts anderes. Du
kannst die Benachrichtigungen jederzeit deaktivieren, mit jeder Mail wird ein
unsubscribe-link mitgeschickt.
"""
db.execute("""INSERT INTO cities(user_id, city, markdown, mail_md,
masto_link, twit_link) VALUES(?,?,?,?,?,?)""",
(self.uid, city, markdown, mail_md, masto_link, twit_link))
db.commit()

View File

@ -1,6 +0,0 @@
import sys
sys.path.insert(0, '/srv/pythonenv/lib/python3.5/site-packages/')
# import web application
from frontend import application