Compare commits
342 commits
Author | SHA1 | Date | |
---|---|---|---|
b3yond | 482350f8c7 | ||
b3yond | 6b52a6303a | ||
b3yond | 2a90573d5e | ||
b3yond | e735936c7a | ||
b3yond | ee9b051c71 | ||
b3yond | 139195fd02 | ||
b3yond | 3dd976ef40 | ||
b3yond | cdecd170a0 | ||
b3yond | ec68f17b32 | ||
b3yond | ddefc2aafa | ||
b3yond | 60e1d8ec30 | ||
b3yond | d5b0ba9b6d | ||
b3yond | 26fa98ad9b | ||
b3yond | de525adb7a | ||
b3yond | 30c49bbfc8 | ||
b3yond | 880b327b20 | ||
b3yond | 467fdaa42a | ||
b3yond | a4996266a1 | ||
b3yond | c9c153117e | ||
b3yond | 54489807da | ||
b3yond | 4b8798ddea | ||
6a5e7f5028 | |||
7507d0392d | |||
b3yond | 4bd99ebb90 | ||
b3yond | 12a0b1efe5 | ||
b3yond | a38c2316f2 | ||
b3yond | 76b3b574f0 | ||
b3yond | 2ce27fc52f | ||
b3yond | 1c8853341a | ||
b3yond | a529f4eb23 | ||
b3yond | 521f0e7ef2 | ||
2bee67bf84 | |||
cb2f3cb2e1 | |||
a47ad74619 | |||
b3yond | f6c19abad6 | ||
b3yond | e7e230b2f0 | ||
b3yond | e72d4872c0 | ||
b3yond | d5823ee1ad | ||
b3yond | 268b9748c3 | ||
b3yond | 8e1234d9b5 | ||
b3yond | 4c61b1ba99 | ||
b3yond | 5a4763366b | ||
b3yond | 945a90c7e1 | ||
b3yond | bc7a4a72f8 | ||
b3yond | d964927a3f | ||
b3yond | 238dd20d20 | ||
b3yond | f274d25822 | ||
b3yond | 710a89c282 | ||
b3yond | 8b36589557 | ||
b3yond | 7cb211b4cb | ||
9508618347 | |||
b3yond | 651e684316 | ||
b3yond | 1a0ae78ac1 | ||
b3yond | 01f33ea29a | ||
b3yond | 400e15d18a | ||
b3yond | 55db252f44 | ||
b3yond | f64142d882 | ||
b3yond | f286c127ba | ||
b3yond | 4428fa932f | ||
b3yond | cc5ab22be5 | ||
b3yond | 56e948b798 | ||
b3yond | c36b8ab673 | ||
b3yond | 17df4f15e4 | ||
b3yond | b5de7cde9f | ||
b3yond | 8eb2d98c03 | ||
b3yond | 9836ec7752 | ||
b3yond | 9e8cfa624c | ||
b3yond | 084049bbfe | ||
b3yond | 6a8cf5c6af | ||
b3yond | de657ba350 | ||
b3yond | bbe27e2586 | ||
b3yond | 9a3c09b119 | ||
b3yond | 30de2196ac | ||
b3yond | 9ca521493a | ||
b3yond | 0449d892a3 | ||
b3yond | f59be986e2 | ||
b3yond | 79d5a6f112 | ||
b3yond | 9800b52153 | ||
b3yond | 54930a32f6 | ||
b3yond | 0b862e35c8 | ||
b3yond | 13fcb41148 | ||
b3yond | d3d7bd098d | ||
b3yond | 8a4cc17575 | ||
b3yond | 3d669e6caf | ||
b3yond | e032ecbcc3 | ||
b3yond | 30f1f8a21c | ||
b3yond | 942f19fefe | ||
b3yond | 5119c6bfbb | ||
b3yond | 2068b99b87 | ||
b3yond | 4851fc0b63 | ||
b3yond | b9a4899981 | ||
b3yond | 304d83ffad | ||
b3yond | 9b3efd7bd2 | ||
b3yond | 24598f0b87 | ||
b3yond | dc8f51c632 | ||
b3yond | 34d43f1911 | ||
b3yond | 2a4d517f1d | ||
b3yond | 2d12aa7107 | ||
b3yond | 47a7452eb4 | ||
b3yond | 234ed59049 | ||
b3yond | 5efea773b8 | ||
b3yond | 3b19278774 | ||
b3yond | 03033d26d7 | ||
b3yond | b35e885ae2 | ||
b3yond | d61d5750bb | ||
b3yond | aa9267e8d1 | ||
b3yond | 732ac1c5d3 | ||
b3yond | b94ead7041 | ||
b3yond | b20a080129 | ||
b3yond | 1412dbc54c | ||
b3yond | f28df3ce3e | ||
b3yond | 0cf1d8b603 | ||
b3yond | 244bde51b6 | ||
b3yond | 36c21dbfbb | ||
b3yond | f360c4f8fd | ||
b3yond | fcab07246b | ||
b3yond | 11f3c5713b | ||
b3yond | 1a793657af | ||
b3yond | e9ac7286d9 | ||
b3yond | 823df7b04a | ||
1703eb3802 | |||
72d6798022 | |||
c2ed73bafc | |||
c576888da5 | |||
b3yond | 25bfe8e838 | ||
b3yond | eae077cb9b | ||
b3yond | 91181e1cf8 | ||
b3yond | 6757e62242 | ||
b3yond | c37a447392 | ||
b3yond | 3e83ba95da | ||
b3yond | faaf8ac5f4 | ||
b3yond | a54538bcea | ||
b3yond | 6f3c953736 | ||
b3yond | 0624bcb378 | ||
b3yond | 6cac81e444 | ||
f68a869309 | |||
b3yond | a0bd5e69e1 | ||
b3yond | 4b953f54e5 | ||
8acbfb4569 | |||
b3yond | 439dbeb1fa | ||
b3yond | fd8b29c55f | ||
b3yond | f1d7215eba | ||
b3yond | 4586e14ee4 | ||
b3yond | 40c834020a | ||
b85360b0a8 | |||
185014a452 | |||
b3yond | b5f6854a1c | ||
b3yond | 7ca904564c | ||
b3yond | b80b80dc43 | ||
b3yond | 96329e968e | ||
b3yond | c7aa87cb3b | ||
b3yond | ad4e65e0fa | ||
b3yond | 9c599cec37 | ||
b3yond | 848b7b1cb5 | ||
0ffe4daac8 | |||
b3yond | 372e0612a6 | ||
b3yond | 57a2e4dcb1 | ||
b3yond | ec399db2eb | ||
b3yond | ef0ce8f9f1 | ||
b3yond | 27b63d9f8f | ||
b3yond | cc0b3378a9 | ||
b3yond | d002969377 | ||
b3yond | 1f0583da74 | ||
b3yond | e1eb737ad0 | ||
b3yond | 9beb864a2f | ||
b3yond | a05205289f | ||
b3yond | 89fce872f3 | ||
b3yond | d7eba3d233 | ||
b3yond | 10b3550ad6 | ||
b3yond | 2d879383d4 | ||
b3yond | 4343be7e06 | ||
b3yond | 55a804f0d6 | ||
b3yond | cae74a5715 | ||
b3yond | 3d23b47a6e | ||
57a4a50254 | |||
a8504971ea | |||
b3yond | 9db71e485d | ||
b3yond | 8dfffffe76 | ||
b3yond | 4fb2930c6c | ||
b3yond | fd8f236cdd | ||
b3yond | 44cd1308ba | ||
b3yond | 4c6ab2d3ae | ||
b3yond | 0719b094f8 | ||
a48ba9ebf8 | |||
4b37c0df3d | |||
b3yond | 83d8700e30 | ||
86d63fe9a0 | |||
b3yond | bfd9a2d5fe | ||
b3yond | 7543bf3e6e | ||
b3yond | cd5eeb3917 | ||
b3yond | f4736c91dd | ||
b3yond | 559b709b8f | ||
b3yond | 628fcb4f95 | ||
b3yond | 2a9c5c657f | ||
b3yond | c9dfb6611a | ||
b3yond | 04a6b82c1b | ||
29a577508f | |||
b3yond | d4d58daf40 | ||
b3yond | 48d44cf698 | ||
b3yond | 9274dfdecb | ||
b3yond | 5ec4d1aab0 | ||
b3yond | 62eb588b28 | ||
b3yond | 9885e39d68 | ||
b3yond | 01b3657c8e | ||
6996cbfc09 | |||
b3yond | 591020f8cc | ||
b3yond | d706c4f1cc | ||
642cf429e5 | |||
b3yond | dd24a2b265 | ||
b3yond | 3afa73ccaf | ||
b3yond | 1a76cba4fb | ||
b3yond | 84746a6d01 | ||
b3yond | 064ca181c0 | ||
b3yond | 16580f3181 | ||
b3yond | 7f8697947c | ||
b3yond | 758ff1db46 | ||
b3yond | 9b01ac7eac | ||
b3yond | 898f229145 | ||
b3yond | 0b41b43421 | ||
b3yond | 24beedf467 | ||
fd2a389d12 | |||
b3yond | 25c57039ea | ||
b3yond | 20cfe159e9 | ||
57cf3bd7d6 | |||
1af14a5db4 | |||
22de5e7e4e | |||
b3yond | 4d556ec595 | ||
b3yond | bf7c21c113 | ||
b3yond | 45d4cd2062 | ||
b3yond | 034513718f | ||
b3yond | 261496c097 | ||
b3yond | 19cc64d00d | ||
b3yond | 27497e7129 | ||
d280130b29 | |||
78331212e6 | |||
b3yond | 10fb150c21 | ||
b3yond | 29c35be8a5 | ||
bfc311b6c9 | |||
4981223ee8 | |||
9339015101 | |||
4850860f82 | |||
b3yond | c9fd91de74 | ||
b3yond | affd209a3b | ||
b3yond | ca55223be9 | ||
b3yond | 5670c92d33 | ||
2b6b3a2263 | |||
b3yond | 0aa1d79621 | ||
b3yond | 788f55860b | ||
b3yond | ba6e13a2be | ||
8e08eb9c2e | |||
b3yond | c71bc8574a | ||
b3yond | bc41d7460c | ||
b3yond | 9425fde917 | ||
88afab1270 | |||
5db529702c | |||
b3yond | 66bb1f86a3 | ||
b3yond | 49bd00fba3 | ||
b3yond | ec3053a0ab | ||
036c742f34 | |||
1dd75c10d5 | |||
890e720c91 | |||
a3e33c36c6 | |||
670a1a6d8f | |||
b3yond | 51dec7e072 | ||
c3f9f86d3f | |||
b3yond | 2d7b222c21 | ||
dde4e6af7b | |||
cb764f2ec3 | |||
b3yond | d207d4e960 | ||
b3yond | 5d2ffbd935 | ||
ce79b37b38 | |||
2fdc6f1f28 | |||
f99b44d815 | |||
c980e7abb5 | |||
b3yond | 95ada7ba62 | ||
b3yond | 9ac7ab3b70 | ||
b3yond | 64f1fff275 | ||
3ea06d1e93 | |||
daf6fe831f | |||
751f9154cc | |||
b3yond | 061fb62bdc | ||
305fb8e06a | |||
b3yond | ba9b28f254 | ||
b3yond | a8efcd7825 | ||
b3yond | 17d044ec20 | ||
b3yond | be118fb4bd | ||
b3yond | aa5669b019 | ||
b3yond | 5f55eb88ff | ||
b3yond | c548a81272 | ||
b3yond | a65d410e4f | ||
b3yond | 570792ba37 | ||
b3yond | c612a9dee0 | ||
b3yond | bd2599c91a | ||
b3yond | 81e2357e2f | ||
b3yond | a3b74dcfff | ||
b3yond | 32e86a3c0e | ||
b3yond | b9613a60de | ||
404be47d1b | |||
b3yond | 235b8524f8 | ||
b3yond | 9e09dcea84 | ||
b3yond | c48704ea73 | ||
b3yond | cdc88e3ee3 | ||
b3yond | ee8040893e | ||
b3yond | 390f4dc76e | ||
b3yond | a176f856d8 | ||
b3yond | 529270a396 | ||
3f4ec83abe | |||
b3yond | f9033a009f | ||
b3yond | eb0252f235 | ||
b3yond | 9cc2bf4228 | ||
b3yond | 28891d5069 | ||
b3yond | 9e70ff6866 | ||
b3yond | 87302faf9e | ||
b3yond | 8a7c2f0110 | ||
b3yond | 7bbcbe1ab1 | ||
b3yond | 4b21dddddf | ||
b3yond | 26d1282413 | ||
b3yond | 79f301d823 | ||
b3yond | 2ce2a45f7b | ||
b3yond | 52c2d1e341 | ||
b3yond | 1f01938a8c | ||
b3yond | a7bae0aed9 | ||
b3yond | 1b75e03fc5 | ||
b3yond | 7ccf6917c8 | ||
b3yond | 2e89f9bf2d | ||
b3yond | fb36221a40 | ||
b3yond | 0c04ce4b70 | ||
b3yond | 1e0a8a09ed | ||
b3yond | 27902954e8 | ||
b3yond | 821f201454 | ||
b3yond | 3bc1010edf | ||
b3yond | 0ba2438541 | ||
b3yond | c9d5f7441a | ||
b3yond | ace28ee25a | ||
b3yond | 63cf134ffa | ||
b3yond | 9ef0b27970 | ||
b3yond | 5feb6cf5be | ||
b3yond | da421769e9 | ||
b3yond | 89ce129b38 | ||
b3yond | 2e80d10222 | ||
b3yond | 2b4d8650c9 | ||
b3yond | 7689eb25f8 |
10
.editorconfig
Normal file
|
@ -0,0 +1,10 @@
|
|||
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
|
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual Behavior**
|
||||
A clear and concise description of what happens.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Ticketfrei Version**
|
||||
See the commit on which Ticketfrei is running at example.org/version.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
7
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: Something else
|
||||
about: Other ideas?
|
||||
|
||||
---
|
||||
|
||||
*If your suggestion is neither a bug report nor a feature request, this is the right place. Just describe what you have in mind.*
|
3
.gitignore
vendored
|
@ -5,13 +5,16 @@ __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/
|
||||
|
|
1
LICENSE
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
|
281
README.md
|
@ -1,96 +1,249 @@
|
|||
# Ticketfrei micro messaging bot
|
||||
# Ticketfrei social bot
|
||||
|
||||
Version: 1.0
|
||||
Ticketfrei is a mastodon/twitter/mail bot to dodge ticket controllers in public
|
||||
transport systems.
|
||||
|
||||
<!-- This mastodon/twitter bot has one purpose - breaking the law. -->
|
||||
## Mission
|
||||
|
||||
The functionality is simple: it retweets every tweet where it is
|
||||
mentioned.
|
||||
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.**
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 =/
|
||||
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!
|
||||
|
||||
Website: https://wiki.links-tech.org/IT/Ticketfrei
|
||||
## How It Works
|
||||
|
||||
## Install
|
||||
The functionality is simple: It retweets every tweet where it is mentioned.
|
||||
|
||||
Setting up a ticketfrei bot for your city is quite easy. Here are the
|
||||
few steps:
|
||||
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.
|
||||
|
||||
First you need to install python3 and virtualenv with your favourite
|
||||
package manager.
|
||||
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.
|
||||
|
||||
Create and activate virtualenv:
|
||||
In version 2, this repository contains a web application. On this website,
|
||||
people can register an own bot for their city - the website manages multiple
|
||||
bots for multiple citys. This way, you do not have to host it yourself.
|
||||
|
||||
In the promotion folder, you'll find some promotion material you can use to
|
||||
build up such a community in your city. Unfortunately it is in german - but
|
||||
it's editable, feel free to translate it!
|
||||
|
||||
Website (our flagship instance): https://ticketfrei.links-tech.org
|
||||
|
||||
More information: https://wiki.links-tech.org/IT/Ticketfrei
|
||||
|
||||
## Do you want Ticketfrei in your city?
|
||||
|
||||
Just go to https://ticketfrei.links-tech.org or another website where this software is
|
||||
running.
|
||||
|
||||
* 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
|
||||
|
||||
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.
|
||||
|
||||
To this date, we have never heard of this happening though.
|
||||
|
||||
### Blocklisting
|
||||
|
||||
You also need to edit the goodlist and the blocklist. You can do this on the
|
||||
website, in the settings of your bot.
|
||||
|
||||
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.
|
||||
|
||||
## Do you want to offer a Ticketfrei website to others?
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Install the necessary packages, create and activate virtualenv:
|
||||
|
||||
```shell
|
||||
sudo apt install python3 virtualenv
|
||||
virtualenv -p python3 .
|
||||
. bin/activate
|
||||
```
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```shell
|
||||
pip install tweepy pytoml requests Mastodon.py
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
This configuration is only for the admin. Moderators can log into
|
||||
twitter/mastodon/mail and configure their personal bot on the settings page.
|
||||
|
||||
You have to configure all of the accounts via config.toml; it should
|
||||
be fairly intuitive to enter the right values.
|
||||
|
||||
## 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. So it won't retweet their future tweets and the useless
|
||||
retweet is deleted if someone tries to check if something was
|
||||
retweeted in the last hour or something.
|
||||
|
||||
To this date, we have never heard of this happening though.
|
||||
|
||||
### blacklisting
|
||||
|
||||
You also need to edit the goodlist and the blacklist. They are in the
|
||||
"goodlists" and "blacklists" folders. All text files in those
|
||||
directories will be used, so you should delete our templates; but
|
||||
feel free to use them as an orientation.
|
||||
|
||||
Just add the words to the goodlist, which you want to require. A
|
||||
report is only spread, if it contains at least one of them. If you
|
||||
want to RT everything, just add a ```*```.
|
||||
|
||||
There is also a blacklist, which you can use to automatically sort
|
||||
out malicious tweets. Be careful though, our filter can't read the
|
||||
intention with which a word was used. Maybe you wanted it there.
|
||||
|
||||
### screen
|
||||
|
||||
To keep the bots running when you are logged out of the shell, you
|
||||
can use screen:
|
||||
Set up LetsEncrypt:
|
||||
|
||||
```shell
|
||||
sudo apt-get install screen
|
||||
echo "if [ -z "$STY" ]; then screen -RR; fi" >> ~/.bash_login
|
||||
screen
|
||||
python3 ticketfrei.py
|
||||
sudo apt-get install python-certbot-nginx -t stretch-backports
|
||||
sudo certbot --authenticator webroot --installer nginx --agree-tos --redirect --hsts
|
||||
```
|
||||
|
||||
To log out of the screen session, press "ctrl+a", and then "d".
|
||||
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 socket & database
|
||||
sudo mkdir /var/ticketfrei
|
||||
sudo chown www-data:www-data -R /var/ticketfrei
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
### 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 &
|
||||
```
|
||||
|
||||
|
|
14
active_bots/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
__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)
|
98
active_bots/mailbot.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
#!/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(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
if msg.is_multipart():
|
||||
text = []
|
||||
for part in msg.get_payload():
|
||||
if part.get_content_type() == "text":
|
||||
text.append(part.get_payload())
|
||||
elif part.get_content_type() == "application/pgp-signature":
|
||||
pass # ignore PGP signatures
|
||||
elif part.get_content_type() == "multipart/mixed":
|
||||
for p in part:
|
||||
if isinstance(p, str):
|
||||
text.append(p)
|
||||
elif p.get_content_type() == "text":
|
||||
text.append(part.get_payload())
|
||||
else:
|
||||
logger.error("unknown MIMEtype: " +
|
||||
p.get_content_type())
|
||||
else:
|
||||
logger.error("unknown MIMEtype: " +
|
||||
part.get_content_type())
|
||||
text = '\n'.join(text)
|
||||
else:
|
||||
text = msg.get_payload()
|
||||
post = report.Report(author, "mail", text, None, date)
|
||||
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()
|
73
active_bots/mastodonbot.py
Executable file
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from bot import Bot
|
||||
import logging
|
||||
from mastodon import Mastodon
|
||||
import re
|
||||
from report import Report
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MastodonBot(Bot):
|
||||
def crawl(self, user):
|
||||
"""
|
||||
Crawl mentions from Mastodon.
|
||||
|
||||
:return: list of statuses
|
||||
"""
|
||||
mentions = []
|
||||
try:
|
||||
m = Mastodon(*user.get_masto_credentials())
|
||||
except TypeError:
|
||||
# logger.error("No Mastodon Credentials in database.", exc_info=True)
|
||||
return mentions
|
||||
try:
|
||||
notifications = m.notifications()
|
||||
except Exception:
|
||||
logger.error("Unknown Mastodon API Error.", exc_info=True)
|
||||
return mentions
|
||||
for status in notifications:
|
||||
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']))
|
||||
return mentions
|
||||
|
||||
def post(self, user, report):
|
||||
try:
|
||||
m = 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)
|
73
active_bots/telegrambot.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from bot import Bot
|
||||
import logging
|
||||
from report import Report
|
||||
from twx.botapi import TelegramBot as Telegram
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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").wait()
|
||||
except TypeError:
|
||||
updates = tb.get_updates().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
|
||||
if isinstance(update, int):
|
||||
try:
|
||||
logger.error("City " + str(user.uid) +
|
||||
": Unknown Telegram error code: " +
|
||||
str(update) + " - " + str(updates[1]))
|
||||
except TypeError:
|
||||
logger.error("Unknown Telegram error code: " + str(update))
|
||||
return reports
|
||||
user.save_seen_tg(update.update_id)
|
||||
if update.message.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)
|
74
active_bots/twitterDMs.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
import tweepy
|
||||
import re
|
||||
import requests
|
||||
import report
|
||||
from time import time
|
||||
from bot import Bot
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitterDMListener(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:
|
||||
return reports # no twitter account for this user.
|
||||
last_dm = user.get_seen_dm()
|
||||
try:
|
||||
if last_dm is None:
|
||||
mentions = api.direct_messages()
|
||||
else:
|
||||
mentions = api.direct_messages(since_id=last_dm[0])
|
||||
user.set_last_twitter_request(time())
|
||||
for status in mentions:
|
||||
text = re.sub(
|
||||
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
|
||||
"", status.text)
|
||||
reports.append(report.Report(status.author.screen_name,
|
||||
"twitterDM",
|
||||
text,
|
||||
status.id,
|
||||
status.created_at))
|
||||
user.save_seen_dm(last_dm)
|
||||
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 as terror:
|
||||
# Waiting for https://github.com/tweepy/tweepy/pull/1109 to get
|
||||
# merged, so direct messages work again
|
||||
if terror.api_code == 34:
|
||||
return reports
|
||||
logger.error("Twitter API Error: General Error", exc_info=True)
|
||||
return reports
|
||||
|
||||
def post(self, user, report):
|
||||
pass
|
90
active_bots/twitterbot.py
Executable file
|
@ -0,0 +1,90 @@
|
|||
#!/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(__name__)
|
||||
|
||||
|
||||
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", exc_info=True)
|
||||
return []
|
||||
|
||||
def post(self, user, report):
|
||||
try:
|
||||
api = self.get_api(user)
|
||||
except IndexError:
|
||||
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
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
44
backend.py
Executable file
|
@ -0,0 +1,44 @@
|
|||
#!/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
|
||||
|
||||
|
||||
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()
|
||||
fh = logging.FileHandler('/var/log/ticketfrei/backend.log')
|
||||
fh.setLevel(logging.DEBUG)
|
||||
logger.addHandler(fh)
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
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()
|
|
@ -1,13 +0,0 @@
|
|||
bastard
|
||||
bitch
|
||||
whore
|
||||
hitler
|
||||
slut
|
||||
hure
|
||||
jude
|
||||
schwuchtel
|
||||
fag
|
||||
faggot
|
||||
nigger
|
||||
neger
|
||||
schlitz
|
9
bot.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
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
|
70
config.py
Executable file
|
@ -0,0 +1,70 @@
|
|||
import pytoml as toml
|
||||
import os
|
||||
|
||||
|
||||
def load_env():
|
||||
"""
|
||||
load environment variables from the environment. If empty, use default
|
||||
values from config.toml.example.
|
||||
|
||||
:return: config dictionary of dictionaries.
|
||||
"""
|
||||
with open('config.toml.example') as defaultconf:
|
||||
configdict = toml.load(defaultconf)
|
||||
|
||||
try:
|
||||
if os.environ['CONSUMER_KEY'] != "":
|
||||
configdict['twitter']['consumer_key'] = os.environ['CONSUMER_KEY']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['CONSUMER_SECRET'] != "":
|
||||
configdict['twitter']['consumer_secret'] = os.environ['CONSUMER_SECRET']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['HOST'] != "":
|
||||
configdict['web']['host'] = os.environ['HOST']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['PORT'] != "":
|
||||
configdict['web']['port'] = os.environ['PORT']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['CONTACT'] != "":
|
||||
configdict['web']['contact'] = os.environ['CONTACT']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['MBOX_USER'] != "":
|
||||
configdict['mail']['mbox_user'] = os.environ['MBOX_USER']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['DB_PATH'] != "":
|
||||
configdict['database']['db_path'] = os.environ['DB_PATH']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return configdict
|
||||
|
||||
|
||||
# read config in TOML format (https://github.com/toml-lang/toml#toml)
|
||||
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]))
|
|
@ -1,48 +1,16 @@
|
|||
[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]
|
||||
[twitter]
|
||||
# You get those keys when you follow these steps:
|
||||
# https://developer.twitter.com/en/docs/basics/authentication/guides/access-tokens
|
||||
consumer_key = "your_consumer_key"
|
||||
consumer_secret = "your_consumer_secret"
|
||||
|
||||
[tuser]
|
||||
enabled = 'false' # set to true if you want to use Twitter
|
||||
# You get those keys when you follow these steps:
|
||||
# https://developer.twitter.com/en/docs/basics/authentication/guides/access-tokens
|
||||
access_token_key = "your_access_token_key"
|
||||
access_token_secret = "your_acces_token_secret"
|
||||
[web]
|
||||
host = "0.0.0.0" # will be used by bottle as a host.
|
||||
port = 80
|
||||
contact = "b3yond@riseup.net"
|
||||
|
||||
[mail]
|
||||
enabled = 'false' # set to true if you want to use Mail notifications
|
||||
# This is the mail the bot uses to send emails.
|
||||
mailserver = "smtp.riseup.net"
|
||||
user = "ticketfrei"
|
||||
passphrase = "sup3rs3cur3"
|
||||
# If you want to receive crash reports (so you can restart the bot
|
||||
# when it breaks down), you should specify a contact email address:
|
||||
#contact = "your_mail@riseup.net"
|
||||
# Mailing list where you want to send warnings to
|
||||
list = "yourcity_ticketfrei@lists.links-tech.org"
|
||||
mbox_user = "root"
|
||||
|
||||
[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'
|
||||
[database]
|
||||
db_path = "/var/ticketfrei/db.sqlite"
|
||||
|
|
301
db.py
Normal file
|
@ -0,0 +1,301 @@
|
|||
from config import config
|
||||
import jwt
|
||||
import logging
|
||||
from os import urandom
|
||||
from pylibscrypt import scrypt_mcf
|
||||
import sqlite3
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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):
|
||||
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")
|
||||
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'])
|
31
deployment/example.org.conf
Normal file
|
@ -0,0 +1,31 @@
|
|||
server {
|
||||
|
||||
listen 443 ssl;
|
||||
server_name example.org;
|
||||
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
access_log /var/log/nginx/example.org_access.log;
|
||||
error_log /var/log/nginx/example.org_error.log;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
|
||||
uwsgi_pass unix:///var/run/ticketfrei/ticketfrei.sock;
|
||||
}
|
||||
|
||||
location /.well-known/acme-challenge {
|
||||
root /var/www/acme;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.org;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
17
deployment/ticketfrei-backend.service
Normal file
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Ticketfrei Backend
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/srv/ticketfrei
|
||||
ExecStart=/srv/ticketfrei/bin/python3 backend.py
|
||||
# Requires systemd version 211 or newer
|
||||
#RuntimeDirectory=uwsgi
|
||||
Restart=always
|
||||
KillSignal=SIGQUIT
|
||||
Type=simple
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
17
deployment/ticketfrei-web.service
Normal file
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Ticketfrei Web Application
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/srv/ticketfrei
|
||||
ExecStart=/usr/bin/uwsgi --ini /srv/ticketfrei/deployment/uwsgi.ini
|
||||
# Requires systemd version 211 or newer
|
||||
RuntimeDirectory=uwsgi
|
||||
Restart=always
|
||||
KillSignal=SIGQUIT
|
||||
Type=notify
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
11
deployment/uwsgi.ini
Normal file
|
@ -0,0 +1,11 @@
|
|||
[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
|
277
frontend.py
Executable file
|
@ -0,0 +1,277 @@
|
|||
#!/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
|
|
@ -1,17 +0,0 @@
|
|||
kontroll?e
|
||||
konti
|
||||
db
|
||||
vgn
|
||||
vag
|
||||
zivil
|
||||
sicherheit
|
||||
uniform
|
||||
station
|
||||
bus
|
||||
bahn
|
||||
tram
|
||||
linie
|
||||
nuernberg
|
||||
nürnberg
|
||||
s\d
|
||||
u\d\d?
|
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -1,43 +0,0 @@
|
|||
# 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
|
@ -1,4 +0,0 @@
|
|||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
211
mailbot.py
|
@ -1,211 +0,0 @@
|
|||
#!/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)
|
|
@ -1,45 +0,0 @@
|
|||
# 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?
|
|
@ -1,18 +0,0 @@
|
|||
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
|
|
@ -1,148 +0,0 @@
|
|||
# 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!
|
||||
|
||||
|
Before Width: | Height: | Size: 330 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 2.9 MiB |
Before Width: | Height: | Size: 3 MiB |
Before Width: | Height: | Size: 244 KiB |
Before Width: | Height: | Size: 467 KiB |
Before Width: | Height: | Size: 2.1 MiB |
16
report.py
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
class Report(object):
|
||||
"""
|
||||
A ticketfrei report object.
|
||||
|
@ -13,24 +14,13 @@ class Report(object):
|
|||
Constructor of a ticketfrei report
|
||||
|
||||
:param author: username of the author
|
||||
:param source: mastodon, twitter, or email
|
||||
:param source: mastodon, twitter, or email bot object
|
||||
:param text: the text of the report
|
||||
:param id: id in the network
|
||||
:param timestamp: time of the report
|
||||
"""
|
||||
self.author = author
|
||||
self.type = source
|
||||
self.source = 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
|
||||
|
|
157
retootbot.py
|
@ -1,157 +0,0 @@
|
|||
#!/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
|
@ -1,237 +0,0 @@
|
|||
#!/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)
|
82
sendmail.py
|
@ -1,75 +1,31 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import smtplib
|
||||
import ssl
|
||||
import pytoml as toml
|
||||
from config import config
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import logging
|
||||
from getpass import getuser
|
||||
import smtplib
|
||||
from socket import getfqdn
|
||||
|
||||
|
||||
class Mailer(object):
|
||||
"""
|
||||
Maintains the connection to the mailserver and sends text to users.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Creates an SMTP client to send a mail. Is called only once
|
||||
when you actually want to send a mail. After you sent the
|
||||
mail, the SMTP client is shut down again.
|
||||
|
||||
:param config: The config file generated from config.toml
|
||||
"""
|
||||
# This generates the From address by stripping the part until the first
|
||||
# period from the mail server address and won't work always.
|
||||
self.fromaddr = config["mail"]["user"] + "@" + \
|
||||
config["mail"]["mailserver"].partition(".")[2]
|
||||
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))
|
||||
|
||||
# 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
|
||||
with smtplib.SMTP('localhost') as smtp:
|
||||
smtp.send_message(msg)
|
||||
|
||||
|
||||
# For testing:
|
||||
if __name__ == '__main__':
|
||||
# read config in TOML format (https://github.com/toml-lang/toml#toml)
|
||||
with open('config.toml') as configfile:
|
||||
config = toml.load(configfile)
|
||||
|
||||
m = Mailer(config)
|
||||
print(m.send("This is a test mail.", m.fromaddr, "Test"))
|
||||
sendmail(config['web']['contact'], "Test Mail",
|
||||
body="This is a test mail.")
|
||||
|
|
32
session.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
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
|
106
static/bot.html
Normal file
|
@ -0,0 +1,106 @@
|
|||
<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>
|
47
static/css/style.css
Normal file
|
@ -0,0 +1,47 @@
|
|||
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;
|
||||
}
|
BIN
static/img/ticketfrei-og-image.jpg
Normal file
After Width: | Height: | Size: 628 KiB |
BIN
static/img/ticketfrei_logo.png
Normal file
After Width: | Height: | Size: 24 KiB |
82
static/index.html
Normal file
|
@ -0,0 +1,82 @@
|
|||
<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>
|
||||
|
||||
<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>
|
333
static/jquery-ui-1.12.1/AUTHORS.txt
Normal file
|
@ -0,0 +1,333 @@
|
|||
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>
|
43
static/jquery-ui-1.12.1/LICENSE.txt
Normal file
|
@ -0,0 +1,43 @@
|
|||
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.
|
11008
static/jquery-ui-1.12.1/external/jquery/jquery.js
vendored
Normal file
BIN
static/jquery-ui-1.12.1/images/ui-icons_444444_256x240.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
static/jquery-ui-1.12.1/images/ui-icons_555555_256x240.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
static/jquery-ui-1.12.1/images/ui-icons_777620_256x240.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
static/jquery-ui-1.12.1/images/ui-icons_777777_256x240.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
static/jquery-ui-1.12.1/images/ui-icons_cc0000_256x240.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
static/jquery-ui-1.12.1/images/ui-icons_ffffff_256x240.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
559
static/jquery-ui-1.12.1/index.html
Normal file
|
@ -0,0 +1,559 @@
|
|||
<!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 "a"">
|
||||
</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'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>
|
1312
static/jquery-ui-1.12.1/jquery-ui.css
vendored
Normal file
18706
static/jquery-ui-1.12.1/jquery-ui.js
vendored
Normal file
7
static/jquery-ui-1.12.1/jquery-ui.min.css
vendored
Normal file
13
static/jquery-ui-1.12.1/jquery-ui.min.js
vendored
Normal file
886
static/jquery-ui-1.12.1/jquery-ui.structure.css
vendored
Normal file
|
@ -0,0 +1,886 @@
|
|||
/*!
|
||||
* 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;
|
||||
}
|
5
static/jquery-ui-1.12.1/jquery-ui.structure.min.css
vendored
Normal file
443
static/jquery-ui-1.12.1/jquery-ui.theme.css
vendored
Normal file
|
@ -0,0 +1,443 @@
|
|||
/*!
|
||||
* 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;
|
||||
}
|
5
static/jquery-ui-1.12.1/jquery-ui.theme.min.css
vendored
Normal file
74
static/jquery-ui-1.12.1/package.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"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": []
|
||||
}
|
39
static/js/functions.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
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"));
|
2
static/js/jquery-3.3.1.min.js
vendored
Normal file
38
static/register.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<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>
|
17
template/city.tpl
Normal file
|
@ -0,0 +1,17 @@
|
|||
% 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}}
|
9
template/login-plain.tpl
Normal file
|
@ -0,0 +1,9 @@
|
|||
<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>
|
2
template/login.tpl
Normal file
|
@ -0,0 +1,2 @@
|
|||
% rebase('template/wrapper.tpl', title='Login')
|
||||
% include('template/login-plain.tpl')
|
16
template/mail.tpl
Normal file
|
@ -0,0 +1,16 @@
|
|||
% 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>
|
71
template/propaganda.tpl
Normal file
|
@ -0,0 +1,71 @@
|
|||
% 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>
|
||||
|
||||
|
||||
|
16
template/register-plain.tpl
Normal file
|
@ -0,0 +1,16 @@
|
|||
<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>
|
||||
|
10
template/register.tpl
Normal file
|
@ -0,0 +1,10 @@
|
|||
% 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
|
164
template/settings.tpl
Normal file
|
@ -0,0 +1,164 @@
|
|||
% 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>
|
||||
|
||||
|
30
template/wrapper.tpl
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!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>
|
|
@ -1,61 +0,0 @@
|
|||
#!/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
|
@ -1,75 +0,0 @@
|
|||
#!/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)
|
468
user.py
Normal file
|
@ -0,0 +1,468 @@
|
|||
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
|
||||
|
||||
|
||||
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
|
||||
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().splitlines():
|
||||
if word in badwords:
|
||||
return False
|
||||
for word in default_badwords.splitlines():
|
||||
if word in badwords:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_last_twitter_request(self):
|
||||
db.execute("SELECT date FROM twitter_last_request WHERE user_id = ?;",
|
||||
(self.uid,))
|
||||
return db.cur.fetchone()[0]
|
||||
|
||||
def set_last_twitter_request(self, date):
|
||||
db.execute("UPDATE twitter_last_request SET date = ? WHERE user_id = ?;",
|
||||
(date, self.uid))
|
||||
db.commit()
|
||||
|
||||
def get_telegram_credentials(self):
|
||||
db.execute("""SELECT apikey
|
||||
FROM telegram_accounts
|
||||
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.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()
|