Compare commits

...

467 Commits

Author SHA1 Message Date
Egg c7b34259eb fix logging some more 2023-06-30 13:32:40 +02:00
Egg 32278bed15 fix logging 2023-06-30 13:30:32 +02:00
Egg 97e0566759 out of date and seemingly wrong encoding 2023-06-30 12:58:33 +02:00
Egg d584cb7d79 improve sleep. add error logging to db 2023-06-22 08:14:30 +02:00
Egg 8784a0abf0 switch daemon to rc_exec
otherwise it fails to start
2023-05-28 22:22:39 +02:00
Egg 8c95f5a57f improve logging
adds timestamps, levels to each message.
each time the daemon is started a message is printed too
2023-05-24 18:13:19 +02:00
Egg 81d20bf67c less sleep pls 2023-05-24 17:56:59 +02:00
Egg 60df1dc9ad add missing import 2023-05-24 17:49:39 +02:00
Egg e362495437 Revert "Revert "decrease telegram timeout, add sleep for ratelimit""
This reverts commit 5e371768e3.
2023-05-24 17:49:06 +02:00
Egg 19def1ed31 Revert "decrease telegram timeout, add sleep for ratelimit"
This reverts commit c11c23d44e.
2023-05-24 17:36:35 +02:00
Egg 1acf567916 decrease telegram timeout, add sleep for ratelimit 2023-05-21 23:35:08 +02:00
Egg 1037622f3f reordered sleep. updated deployment files 2023-05-21 23:23:37 +02:00
Egg 1486c79645 telegrambot: add a polling timeout aka wait for a while 2023-02-21 10:16:21 +01:00
Egg d698bb2ed0 move sleep to do it for each message we send 2023-02-05 11:53:16 +01:00
Egg a94afbaecf reduce sleep to 0.5 seconds 2023-01-29 11:30:55 +01:00
Egg d5c9981efd add sleep to make backend use less CPU 2023-01-14 23:26:46 +01:00
missytake feadd6ee29 skip if message is none 2021-04-04 11:31:13 +02:00
missytake 7b0d33a457 fixing attributeError, hopefully for good 2021-04-04 01:06:13 +02:00
missytake 27b79354e7 fix AttributeError 2021-04-02 17:33:26 +02:00
missytake 9ddd53fb8f db locks: wait up to 5 seconds before restarting frontend 2021-03-19 19:51:06 +01:00
missytake 5d0cd82f9e Fix import error 2020-08-07 12:39:04 +02:00
missytake c982de4b7f Restart frontend if there are DB lock errors 2020-08-07 12:29:39 +02:00
missytake 9fa6cde752 catch Mastodon Version errors 2020-07-29 21:45:16 +02:00
missytake 7af7c2a18d create initial seen_tweets entry 2020-07-27 02:39:38 +02:00
missytake 1293861b0a error handling and deployment 2020-07-27 01:14:59 +02:00
missytake 9a7eeef600 fix email Message parsing 2020-07-15 21:46:12 +02:00
missytake 337d5d4330 reload aliases table after appending 2020-07-15 21:01:29 +02:00
missytake 26def0a845 twitter DMs don't work anyway 2020-07-15 20:56:45 +02:00
missytake 6b62cd7355 requirements in one file 2020-06-30 17:27:12 +02:00
missytake be72923f07 Repair blocklist (#110)
* fixing blocklist

* added logging of blocklist events

* goodlist logging

* twitter DMs are broken

* fixed blocklist & tested it

* disabling twitterDMs should be in a separate PR.
2020-06-17 10:51:05 +00:00
missytake 9b6c51ebd6 logging network errors 2020-06-16 19:05:07 +02:00
missytake 55082b8592 documented the architecture of Ticketfrei 2 2020-06-10 20:49:00 +02:00
missytake 1ad7f6f130 improved systemd file which recovers more easily from crashes 2020-01-15 19:57:18 +01:00
anon_user 0a30876f19 vmike read the docs & did some magic 2019-11-20 23:15:21 +01:00
anon_user c2df1d83f8 don't spam the log if the network is failing 2019-09-15 07:30:50 +02:00
anon_user bae6836800 Fixed shutdowns when Mastodon Errors can't get instance name 2019-09-14 12:12:16 +02:00
b3yond 6461a011e9 Merge pull request #106 from ticketfrei/masto502
More detailed Mastodon Server errors
2019-08-11 10:06:46 +02:00
b3yond 59f10c50f2 those errors are not unknown of course. 2019-08-09 15:29:51 +02:00
b3yond 87bca89efa added backtrace to general error message 2019-08-09 15:21:49 +02:00
b3yond fc2399346f Merge pull request #104 from ticketfrei/hotfix-tg-2.1.6
Hotfix tg 2.1.6
2019-08-09 15:46:30 +03:00
b3yond e1d1bd91f8 fixing None TypeError 2019-08-09 14:41:14 +02:00
b3yond 4f5f63b20f logging telegram messages for debug purposes 2019-08-09 14:35:01 +02:00
b3yond 6686833ab5 ... sending text reports should of course be allowed. 2019-08-09 12:24:09 +02:00
b3yond b2acc15400 Merge pull request #101 from SchoolGuy/fix-non-text-message-crash
Fix non text message crash
2019-07-22 21:05:37 +02:00
Enno G 86af9e9a9f Change to hasattr 2019-07-22 20:51:49 +02:00
SchoolGuy b6b3aa5bfc Check if the text property is inside the message object. 2019-07-14 15:58:22 +02:00
b3yond 6f823cb016 fixed uwsgi deployment instructions 2019-07-13 09:04:05 +02:00
b3yond 76d758d11c added the server URL to masto server errors 2019-07-08 23:09:32 +02:00
b3yond 981b4a787a more detailed Mastodon 5xx error messages 2019-07-08 23:04:33 +02:00
b3yond 2ca213d88a Created a borgbackup script for deployments with nginx & uwsgi 2019-07-07 19:22:07 +02:00
b3yond 87c263cb3f fixing import error 2019-05-17 20:42:13 +02:00
b3yond b379620765 Merge pull request #86 from ticketfrei/masto502
don't log Mastodon 502 errors.
2019-05-04 12:04:51 +02:00
b3yond 39dbfacf28 Merge pull request #93 from ticketfrei/images
Notify that telegram image reports are not supported. #90
2019-05-04 10:23:34 +02:00
sid 647d5c028c Update active_bots/telegrambot.py
Co-Authored-By: b3yond <b3yond@riseup.net>
2019-05-04 10:22:03 +02:00
b3yond dc188e143c Merge pull request #87 from ticketfrei/fix-none-error
fixed wrong exception
2019-05-03 17:11:25 +02:00
b3yond 3ddc3b35a3 Notify that telegram image reports are not supported. #90 2019-05-03 14:35:06 +02:00
b3yond e66d167a0a don't log Mastodon 502 errors. 2019-05-03 10:07:16 +02:00
b3yond 35bbe5f075 fixed wrong exception 2019-02-19 16:16:21 +01:00
b3yond eb2bf5a063 Merge pull request #82 from ticketfrei/csrf
Building in CSRF prevention
2019-01-27 17:56:53 +01:00
b3yond 342d7d8ad9 Merge branch 'csrf' of github:ticketfrei/ticketfrei into csrf 2019-01-27 17:55:23 +01:00
b3yond 9f1812c911 better crypto 2019-01-27 17:53:37 +01:00
b3yond 31cce5884e cleaning up the code. 2019-01-27 17:39:31 +01:00
b3yond 194d271cbc hardened the token and fixed the signature 2019-01-27 16:31:59 +01:00
b3yond 164f0eae08 added CSRF token to settings template 2019-01-27 16:24:58 +01:00
b3yond 9873f1c15f added CSRF token to settings template 2019-01-27 16:08:45 +01:00
b3yond cf6736eb65 This was a weird merge conflict with my own branch o.0 2019-01-27 16:05:53 +01:00
b3yond 9e6e8aadfe give CSRF token to template engine 2019-01-27 15:56:19 +01:00
b3yond d0feecc9b2 write and read CSRF cookie 2019-01-27 15:39:49 +01:00
b3yond de663b3dc1 write and read CSRF cookie 2019-01-27 14:52:42 +01:00
b3yond bc7dc80b21 found last db.secret and fixed to use the getter 2019-01-27 11:37:21 +01:00
b3yond 91e0873309 removed redundant photo (how did it end up here? I should take a break.) 2019-01-12 01:20:22 +01:00
b3yond 1b7167e1f2 Merge branch 'envs' 2019-01-12 01:09:38 +01:00
b3yond d601399fcf Merge branch 'master' of github:b3yond/ticketfrei 2019-01-12 00:34:13 +01:00
b3yond 1f810b5b06 apparently I didn't find all calls to db.secret 2019-01-12 00:34:03 +01:00
b3yond 1e9ac5665b new default background image 2019-01-12 00:19:02 +01:00
b3yond c663a0d6b7 new default background image 2019-01-12 00:10:55 +01:00
b3yond 7ce809603a Merge pull request #74 from ticketfrei/version-number
Version number
2019-01-11 23:31:25 +01:00
b3yond e36df6e740 Merge pull request #76 from ticketfrei/envs
Use environment variables for config values
2019-01-11 23:25:31 +01:00
b3yond 83d7dd91e6 no need for such a verbose error message. 2019-01-11 15:16:37 +01:00
b3yond 3590aa67a3 fixing shutdown when exim4 is not set up 2019-01-11 14:52:58 +01:00
sid f4f9925b4f Merge pull request #75 from ticketfrei/git-sid-patch-1
Update LICENSE
2019-01-11 13:49:16 +01:00
sid 9e2102c81d Update LICENSE 2019-01-11 13:48:29 +01:00
b3yond 6cb0b07486 updated the issue template 2019-01-11 13:44:27 +01:00
b3yond 054e59e3ee added call to GET version (commit hash) 2019-01-11 13:38:47 +01:00
b3yond 5a782e47fb Merge pull request #72 from ticketfrei/confirm-37
check if account already exists to avoid double use of confirmation mail
2019-01-11 13:33:04 +01:00
b3yond 184cc3b4a4 replaced attribute with get call 2019-01-11 13:23:37 +01:00
b3yond a45dccdc9b nicer error messages 2019-01-11 13:21:47 +01:00
b3yond 7cd0dc845a check if account already exists #37 2019-01-11 12:15:28 +01:00
b3yond e86a7ea612 formatting #70 2019-01-11 11:41:20 +01:00
b3yond 5e198603bd Merge pull request #71 from patcon/patch-1
Add mission to README
2019-01-11 11:39:37 +01:00
Patrick Connolly 15cb948cc1 Add mission to README. 2019-01-07 14:51:37 -05:00
git-sid 5c2ca271d6 Fix pep8 non-compliant linebreak 2019-01-07 19:05:39 +01:00
git-sid fe5c24d7fa Replace 3 dots with ellipsis to save space 2019-01-07 19:05:32 +01:00
b3yond 96b4975b1f fixing the original TypeError 2018-12-31 15:33:50 +01:00
b3yond be568b7827 when you get crashes bc of your log messages -. 2018-12-31 15:32:19 +01:00
b3yond 6a206f1c0f more verbose telegram error messages 2018-12-31 15:27:11 +01:00
b3yond 10836109b0 removed redundant table declaration 2018-12-28 14:43:18 +01:00
b3yond 5e5429dcfe introduce extra var bc can't write to private attribute 2018-11-12 12:32:28 +01:00
b3yond 6d94c1b540 removed wrong comment - not only testing, also docker containers use this 2018-11-07 09:22:02 +01:00
b3yond c6ce423841 setting host to 0.0.0.0 - it never worked with smth else anyway 2018-11-07 01:57:47 +01:00
b3yond a534bc4e06 if an env var is an empty string, use values from example config 2018-11-06 18:08:51 +01:00
b3yond ec2e218655 make config.py output directly applicable 2018-11-06 17:50:57 +01:00
b3yond ffcb2506f6 beauty overhaul of config.py 2018-11-06 16:23:47 +01:00
b3yond e8ac1ca1c4 fix small bug, print current config if directly called #64 2018-11-06 16:22:11 +01:00
b3yond 631396e764 if no config.toml, set config through environment #64 2018-11-06 16:17:47 +01:00
b3yond 25b9108e7f updated example config options + 1 little fix 2018-11-06 08:56:24 +01:00
b3yond 7a7d405072 fix mailbot crash:
File "/srv/ticketfrei/active_bots/mailbot.py", line 37, in post
    if rec not in report.author:
TypeError: argument of type 'NoneType' is not iterable
2018-10-26 18:20:01 +02:00
b3yond 3d869e57ac added 502 to unlogged Telegram error codes 2018-10-26 17:27:00 +02:00
b3yond 6d52400577 polishing the wording of RSS subscription 2018-10-26 17:25:25 +02:00
git-sid 729909bfd3 add rss feed notification option to info page 2018-10-19 08:26:20 +02:00
b3yond 4bb30224a9 add another issue template 2018-10-18 17:09:21 +02:00
b3yond 9fc55f0bbf Merge pull request #57 from ticketfrei/issue-templates
Update issue templates
2018-10-18 17:06:14 +02:00
b3yond f86559c0e1 Update issue templates 2018-10-18 17:04:06 +02:00
b3yond 6688eb9bfd fix screenshot links in default city page 2018-10-13 20:01:51 +02:00
b3yond 71a34863b1 mastodon seen toots work differently now; function deprecated 2018-10-13 19:34:16 +02:00
b3yond e821b7365e reworked front page text 2018-10-13 19:01:54 +02:00
b3yond 5f49dc2b5e brought README.md up to date 2018-10-13 18:56:09 +02:00
b3yond 75f508dcdf excepted return message 34 so it doesn't get logged #39 2018-10-11 22:22:37 +02:00
b3yond 87fe7c1d59 excepted with wrong Exception 2018-10-11 21:30:55 +02:00
b3yond 7fbf7521fc called wrong user method 2018-10-11 21:29:02 +02:00
b3yond 73ce2b53a9 fixing bug; twitterDM object wasn't created 2018-10-11 21:24:53 +02:00
b3yond 9121b49d0a check if mention is in reply to anything #41 2018-10-08 23:32:33 +02:00
b3yond ea95384a10 Revert "crawl the username only once from twitter and save to db #45"
This reverts commit 9836ec7752.
2018-10-08 23:27:45 +02:00
b3yond a89d1ce654 Merge remote-tracking branch 'origin/master' 2018-10-08 22:14:35 +02:00
b3yond 1f09a92c58 crawl the username only once from twitter and save to db #45 2018-10-08 21:31:25 +02:00
b3yond cbf5502f99 fix repost bug 2018-10-08 21:26:39 +02:00
b3yond 8dc6290262 fix repost bug 2018-10-08 15:09:18 +02:00
b3yond 385941a2c6 ignore PGP signatures; I hope those messages get posted now #40 2018-10-08 15:02:27 +02:00
b3yond 35c3052fcd really fix shutdown in #40 2018-10-07 23:28:29 +02:00
b3yond 0337869dd0 fix shutdown in #40 2018-10-07 23:27:06 +02:00
b3yond 2cc5824bb9 Merge pull request #50 from ticketfrei/rate-limit-39
missing newlines in /etc/aliases
2018-10-07 23:05:52 +02:00
b3yond 9b39fb7996 missing newlines in /etc/aliases 2018-10-07 23:01:14 +02:00
b3yond 7a2908f217 Merge pull request #49 from ticketfrei/rate-limit-39
Rate limit 39
2018-10-07 22:19:57 +02:00
b3yond 6f62304e72 insert empty row at account creation 2018-10-07 22:16:00 +02:00
b3yond 86622c66cb reverting #39 - make rate limits per account, not app 2018-10-07 22:10:48 +02:00
b3yond 69f9c169ff fixed sendmail calls 2018-10-07 21:02:48 +02:00
b3yond 131f18d8d0 cleaned up 2018-10-07 19:21:04 +02:00
b3yond 048f109a72 Merge pull request #43 from b3yond/multi-deployment
Merge the multi-deployment branch to master finally, so we can continue development on master!
2018-10-07 19:17:04 +02:00
b3yond cfff54400b changed promotion repo link #21 2018-10-07 19:15:18 +02:00
b3yond 8eec629c56 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-10-07 19:13:50 +02:00
b3yond 26da7821f9 moved promotion to own repository: https://github.com/ticketfrei/promotion #21 2018-10-07 19:13:23 +02:00
b3yond a61c2f952d moved promotion to own repository: https://github.com/ticketfrei/promotion 2018-10-07 19:12:44 +02:00
b3yond ce88e4976d fixing #42 - excepting more Telegram error codes 2018-10-07 19:09:02 +02:00
b3yond 9c0744c9d4 Merge remote-tracking branch 'origin/multi-deployment' into multi-deployment 2018-10-07 18:48:05 +02:00
b3yond 5c13e1711a fixing #40 - treating different message MIMEtypes 2018-10-07 18:47:37 +02:00
b3yond 5c5f6e1dca comments, because the use of this file is not obvious. 2018-10-06 11:58:08 +02:00
b3yond df007cd7e3 globals are in a separate python file now #39 #45 2018-10-06 10:44:07 +02:00
b3yond 6f5db8bb90 fixing #44 - refactoring how mails are sent 2018-10-06 10:20:37 +02:00
b3yond b01012d32f manually merging multi-deployment with master 2018-10-06 09:29:34 +02:00
b3yond 5f24384a9a fixing #38: putting the city into the From address of report mails 2018-10-06 02:46:54 +02:00
b3yond a459ba92c1 fixing #39 - saving last request in global var, not db. 2018-10-06 00:56:12 +02:00
b3yond 76ca4d772a fixed #41 - mention has to be in status text now 2018-10-05 23:40:41 +02:00
b3yond 272fcb2d49 crawl only mentions, no replies 2018-10-05 11:02:12 +02:00
b3yond a70a7593ad payload is extracted later, we need the message object here 2018-10-05 10:56:34 +02:00
b3yond 044185ff91 get text of email, not message object 2018-09-25 11:06:08 +02:00
b3yond 626ea8ab09 only save newer tweets, don't override with older 2018-09-24 23:16:05 +02:00
b3yond c7dd0ac16b save the newest id, not the current 2018-09-24 23:08:29 +02:00
b3yond 2129306758 excepting and logging Twitter Errors to prevent crashes 2018-09-24 23:02:10 +02:00
b3yond 8255b833fb don't require correctly cased mail addresses 2018-09-24 22:40:29 +02:00
b3yond fbeafc55ac get text from db, not rows 2018-09-24 22:27:11 +02:00
b3yond 9a24237e97 repaired seen_tweets, this time 4 real 2018-09-24 22:14:17 +02:00
b3yond bb1319473e repaired seen_tweets 2018-09-24 22:10:23 +02:00
b3yond 37aa6ea0a5 bug in pattern matching 2018-09-24 21:11:28 +02:00
b3yond 74de0ee5da fuck sql 2018-09-24 21:02:10 +02:00
b3yond 955e14b875 1 comma too much 2018-09-24 21:00:55 +02:00
b3yond da2ddc2900 changed toot logic 2018-09-24 20:54:57 +02:00
b3yond f07ef6ae79 weird log logic 2018-09-24 19:58:17 +02:00
b3yond 959edf3c13 transmitted wrong variable 2018-09-24 19:51:23 +02:00
b3yond 25121cf2eb simple var name error 2018-09-24 19:46:21 +02:00
b3yond 7d81f51a54 logging reports for debugging 2018-09-24 19:41:48 +02:00
b3yond 2686860ea2 sqlite3 syntax error because of trailing " 2018-09-24 17:14:23 +02:00
b3yond 531c027f04 added email routing with exim4 via /etc/aliases 2018-09-23 18:53:46 +02:00
b3yond dbcd2cd6ee style guides ftw 2018-09-15 19:30:37 +02:00
b3yond b560290e72 Merge remote-tracking branch 'origin/multi-deployment' into multi-deployment
# Conflicts:
#	template/settings.tpl
2018-09-15 19:20:25 +02:00
b3yond dd1e967569 blacklist -> blocklist #31 to honor zuckerimtank@twitter.com 2018-09-15 19:01:58 +02:00
b3yond 0d9c6439a7 Merge pull request #36 from git-sid/multi-deployment
merge small stylechanges
2018-09-15 18:58:08 +02:00
b3yond 14d982702e right of = should be right of = 2018-09-15 18:50:37 +02:00
b3yond ac85b2e0be peak readability 2018-09-15 18:47:43 +02:00
b3yond ef4be74fd1 fix TypeError when updates == None 2018-09-14 19:59:45 +02:00
git-sid 7e6043f4a2 Make code even more PEP8 compliant
It could be made even more compliant, but that would actually decrease
readability imo.
2018-09-14 12:45:49 +02:00
git-sid d77d68753c WIP: #31 fix: blacklist -> blocklist.
Replace all relevant instances of "blacklist" with blocklist.
Untested due to OS restricitions. Please check before merge.
2018-09-14 09:44:21 +02:00
git-sid 756a9fb676 Make code more pep8 compliant 2018-09-13 17:33:33 +02:00
git-sid 09d5d23c07 small fix 2018-09-13 17:24:19 +02:00
b3yond 26b2fea755 Merge branch 'multi-deployment' of github:b3yond/ticketfrei into multi-deployment 2018-09-09 21:38:22 +02:00
b3yond e5ab088c77 Merge branch 'multi-deployment' of github:git-sid/ticketfrei into multi-deployment 2018-09-09 21:37:30 +02:00
b3yond 228c0f1264 added telegram to default city page text. 2018-09-09 21:36:27 +02:00
b3yond 2e05c9ab91 those error messages are a bit universal. 2018-09-09 21:36:01 +02:00
b3yond 4c38a6c610 Merge pull request #20 from git-sid/multi-deployment
added basic telegram backend support & smaller changes
2018-09-09 21:26:57 +02:00
b3yond 2897287b55 those error messages are a bit universal. 2018-09-09 20:32:10 +02:00
b3yond 120030460e default values are bad practice 2018-09-09 20:28:13 +02:00
b3yond b80fa78a17 fixed seen_toot problem 2018-09-09 20:22:41 +02:00
b3yond 4924519ba7 fixed telegram spam problem!!111 2018-09-09 18:06:12 +02:00
b3yond 9c27c4093a Merge branch 'multi-deployment' of github:git-sid/ticketfrei into multi-deployment
# Conflicts:
#	active_bots/telegrambot.py
2018-09-09 17:52:23 +02:00
b3yond dbfff0bad0 fixing more telegram bugs 2018-09-09 17:51:07 +02:00
git-sid 90ca3a8fa2 added message type filtering -> only text messages get crawled 2018-09-09 17:47:38 +02:00
b3yond 378e11bf59 Merge remote-tracking branch 'origin/multi-deployment' into multi-deployment 2018-09-09 17:29:29 +02:00
b3yond bd804a432d Making Twitter Rate Limiting intelligent #35 2018-09-09 17:29:06 +02:00
git-sid c93c7a47b8 added offset to telegram message polling to prevent duplicated responses 2018-09-09 17:22:00 +02:00
b3yond 2baa42d8f3 telegram troubleshooting and fine-tuning 2018-09-09 16:58:07 +02:00
b3yond ecb9ac659d appropriate success message 2018-09-09 15:45:25 +02:00
b3yond 8389eed9ca debug level was not enough 2018-09-09 15:09:40 +02:00
b3yond 6d70f2bc3b Merge remote-tracking branch 'origin/multi-deployment' into multi-deployment 2018-09-09 14:58:12 +02:00
b3yond 26f720c1a8 logging confirmation links for debug purposes 2018-09-09 14:57:40 +02:00
git-sid 3de157d9bc added twx dependency to README.md instruction 2018-09-09 14:26:46 +02:00
git-sid 885ba8930f fixed telegram api bug (from -> sender) 2018-09-09 13:25:42 +02:00
b3yond 3622c085c1 host is configurable now 2018-09-08 16:31:02 +02:00
b3yond 3ce2084604 Port is configurable now 2018-09-08 16:06:25 +02:00
b3yond e2365735ff Merge pull request #32 from b3yond/mailbot
Rewriting the Mailbot, included subscription mechanism
2018-09-08 11:14:57 +02:00
b3yond d295c42122 finished #23 code. rw city page info display. Unsubscribe = Delete 2018-09-08 11:14:00 +02:00
b3yond adb637c22c BE & FE store secret in DB. Unsubscribing works 2018-09-08 09:33:40 +02:00
b3yond 6edce5ba57 Merge branch 'multi-deployment' of github:b3yond/ticketfrei into multi-deployment 2018-09-01 14:01:18 +02:00
b3yond 76f8241792 wait on rate limit option for twitter APIs 2018-09-01 14:01:03 +02:00
b3yond 813dd406cb Merge pull request #30 from jorgesumle/multi-deployment
HTML fixes
2018-08-11 17:28:00 +02:00
Jorge Maldonado Ventura ad32035dbe HTML fixes 2018-08-11 17:19:12 +02:00
b3yond 882d086a83 wrote unsubscribe function, but BE & FE have different secrets 2018-08-09 15:01:51 +02:00
b3yond da7ead65fa the mailbot can now receive messages from /var/mail/test 2018-08-08 17:09:26 +02:00
b3yond fca383806d started to build a mailbot implementing bot.py 2018-08-08 14:04:31 +02:00
b3yond 4ef9579a7d Advertising mail notifications on the city page 2018-08-08 10:55:51 +02:00
b3yond 6b697ca200 Merge remote-tracking branch 'origin/multi-deployment' into multi-deployment 2018-08-08 10:42:08 +02:00
b3yond 2dece1fddd you can now subscribe to mail notifications! Also db bugfixes. 2018-08-08 10:24:20 +02:00
b3yond e5d0266124 mail subscription confirm functions;
confirmation code & mail template still missing
2018-08-07 15:10:28 +02:00
b3yond 164eb5e7a1 debugging the backend, adding mail subscribe page, finished VAG zeitung 2018-08-02 22:30:57 +02:00
b3yond c1682a5730 markus s. 2018-08-02 22:04:26 +02:00
b3yond 3617ffb94a added mail subscriber function, confirm missing 2018-07-22 13:56:15 +02:00
b3yond e138c68dfa added mail template callback functions 2018-07-22 13:47:56 +02:00
b3yond 19d5c8983f added template for subscribing to mail notifications (untested) 2018-07-21 15:07:47 +02:00
b3yond 70d5c2c260 replaced illegal flyer parts 2018-07-17 23:41:33 +02:00
b3yond a7f2b3847d fixed backend deployment with systemd 2018-07-14 17:17:36 +02:00
b3yond 179bd439a3 took out mailbot for now. development of mailbot continues on the mailbot branch. 2018-07-14 16:49:11 +02:00
b3yond 68363ec979 bots are safely imported in the backend, except twitterDMs 2018-07-14 16:39:53 +02:00
b3yond c7fcb75aa3 removing redundant p tags 2018-07-13 15:55:40 +02:00
b3yond c5221a048a fixing missing /form tag #28 2018-07-13 15:55:14 +02:00
b3yond 3f611a9e6c included aktionsschwarzfahren in promotion newspaper 2018-07-13 15:12:48 +02:00
sid 1d9c5f3c01 modified telegram bot subscribe implementation 2018-06-30 22:11:41 +02:00
sid 0cf8705d2d completed telegram subscriber list functionality 2018-06-30 21:32:22 +02:00
b3yond ce2301c260 fixing settings issue with required forms 2018-06-25 21:14:22 +02:00
b3yond 02a6e0509b Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-06-25 21:13:47 +02:00
b3yond 8efbad7fdb small changes to default markdown 2018-06-25 21:13:43 +02:00
b3yond f626d0d25b you need to checkout the multi-deployment branch, if you're actively developing 2018-06-24 00:26:00 +02:00
b3yond 1334b299e0 added development instructions to README 2018-06-24 00:24:01 +02:00
b3yond 5c4840e9c2 css fixes 2018-06-24 00:12:39 +02:00
b3yond db8110de6e added telegram to frontend 2018-06-24 00:00:48 +02:00
sid 4c91405f68 added /start, /stop, /help command check & small fixes 2018-05-29 07:07:15 +02:00
sid 20bcfa1bf6 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-05-29 00:10:22 +02:00
b3yond 4aa1bf90af updated install instructions & gitignore 2018-05-28 22:26:07 +02:00
sid c9b5fd1d5c Merge branch 'multi-deployment' into multi-deployment 2018-05-28 21:18:22 +02:00
b3yond 152ef16ab1 only markdown shit still missing, is displaying äöü on production server. 2018-05-25 19:21:30 +02:00
b3yond 09a1f0ce5c last markdown fixes, I swear! 2018-05-25 17:12:41 +02:00
b3yond 4aae0ba1df You can now edit markdown in settings. #18 2018-05-25 16:50:02 +02:00
b3yond b43779e6c8 fixed markdiown render issues. Closing #22 2018-05-25 16:31:08 +02:00
b3yond d70a4759e3 more markdown fixes 2018-05-25 16:27:30 +02:00
b3yond 9c02b21fe8 small markdown fixes 2018-05-25 16:15:44 +02:00
b3yond d633506c83 you can now set goodlist & blacklist in settings. render city page #18. fixed #24 and #25. 2018-05-25 15:57:20 +02:00
b3yond 6b05686379 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-05-25 14:45:07 +02:00
b3yond ab8a64d625 You can now set data for the city page. #22 2018-05-25 14:44:45 +02:00
sid 7b8924406d added basic telegram backend support 2018-05-25 02:38:27 +02:00
b3yond 4ca4c563b2 backported the typo fixes by @git-sid in #19 2018-05-24 21:59:58 +02:00
b3yond efb241c377 backported the typo fixes by @git-sid in #19 2018-05-24 21:58:20 +02:00
b3yond 8a2e35821e Add API to get content of the user facing page 2018-04-27 01:20:37 +02:00
b3yond e78733cbc9 displaying city or other titles on various pages. still ugly. 2018-04-26 23:48:26 +02:00
b3yond db10139ae4 BETTER cat images!!11 2018-04-26 22:30:34 +02:00
b3yond e97191fd78 added cat pictures <3333 2018-04-26 22:28:51 +02:00
b3yond db161ad71b fixing file not found 500 error 2018-04-26 22:23:58 +02:00
Tech 091372bf2b merrrrge 2018-04-26 22:07:00 +02:00
b3yond 37c331693f Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-04-26 21:50:56 +02:00
b3yond 8058ced0ad created a user-facing page 2018-04-26 21:50:52 +02:00
Thomas L 9618e5d3c6 serve jquery from own server 2018-04-26 21:00:29 +02:00
b3yond 65af21b313 fixing import error 2018-04-16 09:38:55 +02:00
b3yond 71dc61cc75 removed outdated images 2018-04-15 22:42:20 +02:00
b3yond 52694bca68 changed background image to jpg & more beautiful 2018-04-15 22:41:33 +02:00
b3yond 2cc6122a9e better log messages 2018-04-15 12:11:49 +02:00
b3yond 4acab73266 Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-04-15 11:59:50 +02:00
b3yond 8db4c108ae implemented twitter DMs 2018-04-15 11:58:19 +02:00
b3yond bc742f7dcc twitter & masto sign limit 2018-04-15 11:42:34 +02:00
b3yond 1c13be1ef4 implemented mastodon DMs, city, and backend.shutdown function 2018-04-15 11:26:48 +02:00
b3yond 1445b587f7 save db in a persistent folder, /var/run is not persistent in every OS 2018-04-15 09:41:27 +02:00
b3yond fab61d4dd3 Twitter OAuth dance works now!!1111 wuuuuhhuuuu 2018-04-14 18:16:05 +02:00
b3yond cdabc7f226 fixed db scheme error 2018-04-14 18:12:55 +02:00
b3yond c3229b0825 fixed several id typos 2018-04-14 17:56:48 +02:00
Thomas L 1603bdc102 add error message for empty form. 2018-04-14 17:53:08 +02:00
b3yond 49b9360add fixed small, but nasty bug 2018-04-14 17:49:19 +02:00
b3yond 6ec51142f1 query is a dict, not a function 2018-04-14 17:38:49 +02:00
Thomas L 616da178e8 no return needed, redirect throws 2018-04-14 17:34:43 +02:00
Thomas L 7c433c5ff7 Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-04-14 17:32:15 +02:00
Thomas L 7dff715656 add default triggerpattern 2018-04-14 17:31:53 +02:00
b3yond 22766abfe7 don't do except all -.- rather repair the fcking logging. 2018-04-14 17:31:01 +02:00
b3yond 40a0e968aa request token is a dict, not a string 2018-04-14 17:19:20 +02:00
b3yond 7e44622b81 fixed url() call 2018-04-14 17:00:30 +02:00
b3yond f997128447 generate url with dedicated function 2018-04-14 16:36:57 +02:00
b3yond f57568b4dc fixed config key error 2018-04-14 16:34:02 +02:00
b3yond abad0baa58 added logging for unstable functions 2018-04-14 16:31:45 +02:00
b3yond 3a9770d548 log python errors to extra file 2018-04-14 15:22:05 +02:00
Thomas L 9b5440ee7e fix login and registration. 2018-03-29 21:58:55 +02:00
Thomas L decee6f30d improve sendmail function. 2018-03-29 02:40:22 +02:00
b3yond fd3b7f9beb merge 2018-03-29 02:00:28 +02:00
b3yond 84982c6edf reworked mailbot to implement bot.py 2018-03-29 01:50:05 +02:00
Thomas L 50e643ceed omit bare except. 2018-03-29 01:25:17 +02:00
Thomas L 748b1a2a7c catch some error cases. 2018-03-29 01:13:53 +02:00
Thomas L 449cc2588b fix account confirmation. 2018-03-29 00:59:13 +02:00
Thomas L c91ef55844 use local mail daemon for confirmation links 2018-03-29 00:57:17 +02:00
b3yond f268fcfd48 getting logging stuff 2018-03-29 00:39:45 +02:00
b3yond 6f3880edad changed logging to new scheme in sendmail.py 2018-03-29 00:33:29 +02:00
b3yond 645a17bbcd Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-03-29 00:31:38 +02:00
b3yond 22294c60d0 added logging to sendmail.py 2018-03-29 00:31:21 +02:00
Thomas L 9693d0dcd0 log to stderr 2018-03-29 00:24:56 +02:00
b3yond 17fd18b6c7 changed default config after deployment learnings 2018-03-29 00:21:14 +02:00
b3yond a3b40244b3 Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-03-29 00:13:11 +02:00
b3yond d8b2b811da started mail rewrite 2018-03-29 00:13:00 +02:00
Thomas L 1a4df14b73 fix recursive import 2018-03-29 00:12:19 +02:00
b3yond 85fcd06e26 solved loop bug 2018-03-28 23:50:55 +02:00
b3yond c0c060a7ae Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-03-28 23:47:02 +02:00
b3yond 9d1b7deff3 clean up 2018-03-28 23:46:06 +02:00
Thomas L 73858ef485 cleanup 2018-03-28 23:40:26 +02:00
Tech 80415cd093 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-03-28 23:33:43 +02:00
b3yond 4844b92b9c reworked twitterbot according to new scheme 2018-03-28 23:33:04 +02:00
b3yond b51449a70a clean up after refactor 2018-03-28 22:12:57 +02:00
b3yond 21f1e9ddb3 small bugfixes 2018-03-28 20:24:21 +02:00
Tech c2d04c1b81 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-03-28 18:47:44 +02:00
Thomas L e38a32d888 Refactoring. 2018-03-28 17:36:35 +02:00
Thomas L fbf44525e3 Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-03-27 23:37:19 +02:00
Tech 32855fb50d added deployment instructions and fixed some deployment issues. 2018-03-27 20:02:47 +02:00
Tech e69274a1f1 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-03-27 19:21:47 +02:00
b3yond ff070d47c4 tested deployment and brought learnings to README. added nginx config. 2018-03-27 01:04:07 +02:00
Tech c080a5fd4a Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-03-26 22:24:06 +02:00
b3yond 36d8329dcc removed attribution of author in bridged reports. #2 2018-03-26 21:21:51 +02:00
Tech 5d9dc443d2 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-03-26 20:55:23 +02:00
Thomas L 6177c1c801 Merge branch 'multi-deployment' of dl6tom.de:public/ticketfrei into multi-deployment 2018-03-25 23:42:45 +02:00
b3yond 8f792cbeac Merge pull request #16 from d24phant/multi-deployment
propaganda - added our mission-draft
2018-03-25 22:57:40 +02:00
b3yond 1cd2a6dedb more paragraphs, some typos 2018-03-25 22:49:14 +02:00
d24phant 70b0fff5a1 fixed some fails_pt2
Hopefully.... :)
2018-03-25 22:32:15 +02:00
d24phant 60f54f5a2d Delete propaganda.tpl 2018-03-25 22:31:00 +02:00
d24phant ba31910a86 Fixed some fails
:)
2018-03-25 22:30:17 +02:00
d24phant 660815d7bb Added "our_mission"-draft 2018-03-25 22:06:31 +02:00
b3yond 8ac2b22fde Lorem Ipsum -> actual promotion text 2018-03-25 18:23:42 +02:00
b3yond 6178050059 config format changed 2018-03-25 17:50:28 +02:00
b3yond 6487d6e8ec don't even dare to try it out. I already changed it. :P 2018-03-25 17:31:51 +02:00
Thomas L b7f0a98613 Add .editorconfig 2018-03-24 16:35:16 +01:00
Thomas L b6ed8f9890 cleanup 2018-03-24 16:26:35 +01:00
Thomas L 54aaecfbc1 Add files for deployment. Make testing use memory-DB. 2018-03-24 15:02:11 +01:00
b3yond 454a9d5e4d started reworking the README 2018-03-24 11:58:15 +01:00
Tech de2eeb8756 Merge branch 'multi-deployment' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-03-24 11:33:32 +01:00
b3yond c618cb8d02 removed a lot of unnecessary clutter 2018-03-24 11:32:59 +01:00
b3yond c4f6335bb9 removed a lot of unnecessary clutter 2018-03-24 11:30:12 +01:00
b3yond 18cf2ce312 changed config.toml layout for ticketfrei 2.0 #12 2018-03-24 11:25:14 +01:00
b3yond 714f04cbce renamed retweetbot & retootbot (wtf, those names) 2018-03-23 18:06:59 +01:00
b3yond 8a90f70eb3 changed trigger to work with db 2018-03-23 18:00:05 +01:00
b3yond d9be9f7705 changed twitter to work with db 2018-03-23 17:35:04 +01:00
b3yond f7bce8e1ba removed waste 2018-03-23 17:07:00 +01:00
b3yond b2f50947c9 changing mail to use db, part 1; seen mails 2018-03-23 17:00:52 +01:00
b3yond 878a578dec reworked mastodon to work with frontend. user, config, logging -> new files. 2018-03-23 15:51:52 +01:00
b3yond e6c5d0d745 rename to frontend.py & backend.py 2018-03-23 13:19:39 +01:00
b3yond 30700d2c81 tested Mastodon OAuth Login - works now. 2018-03-23 13:14:06 +01:00
b3yond 4ccd8de63b merging toms commits 2018-03-23 11:21:10 +01:00
b3yond f6daca96eb initializing logger is also done by ticketfrei now 2018-03-23 11:18:00 +01:00
b3yond 52c47dbbfe ticketfrei can be imported now, takes care of loading config & logging 2018-03-23 11:05:24 +01:00
b3yond 81413a6812 added twitter & masto OAuth to new web.py - untested, take care! 2018-03-23 02:28:00 +01:00
Thomas L 86aefcffd4 Use IF NOT EXISTS instead of sqlite specific hack. Clean up formating. Other minor fixes. 2018-03-22 21:15:15 +01:00
b3yond a67d5ecf32 db is persistent now 2018-03-22 11:32:43 +01:00
b3yond fcc80493f2 fixed db init, fixed confirmation mails, added logout button 2018-03-22 11:22:28 +01:00
b3yond 16f7a24118 modified gitignore 2018-03-22 10:37:53 +01:00
b3yond 992a1a1d95 this file should have been ignored, I guess 2018-03-22 10:35:51 +01:00
b3yond 2e67986c59 fixed login error, db is now saved between different test runs 2018-03-22 10:34:53 +01:00
b3yond 23e4d6930b if table "user" doesn't exist, db is created 2018-03-22 10:25:00 +01:00
b3yond d1b14390ec unified formatting of db_init SQL 2018-03-22 10:05:49 +01:00
b3yond 0e99a09ee3 complete db create statement 2018-03-22 09:51:50 +01:00
Thomas L ba5711aefe start refactoring web-frontend. 2018-03-22 02:23:31 +01:00
b3yond 205097b87f added mastodon oauth dance 2018-03-20 21:24:20 +01:00
b3yond 968815f165 added twitter OAuth dance. 2018-03-20 20:00:19 +01:00
b3yond 970b3a7cdd typo 2018-03-18 21:37:34 +01:00
b3yond 2c672a8e15 outline for masto oauth 2018-03-18 21:36:46 +01:00
b3yond be9e7a70bf removed debug messages, small layout changes 2018-03-17 15:57:56 +01:00
b3yond 0e0e81845e good- and blacklist configuration works now! <3 2018-03-16 18:59:40 +01:00
b3yond 35b61ce369 writing good/blacklist to db. Cookies don't work yet 2018-03-16 17:55:27 +01:00
b3yond 0438fe8014 writing goodlist & blacklist -> db 2018-03-16 15:17:12 +01:00
b3yond 06739db87c created twitter/masto OAuth login stub 2018-03-16 14:21:15 +01:00
b3yond bfab3947c7 implemented the enable button 2018-03-16 12:41:34 +01:00
b3yond f7bc84de0c added OpenGraph data, started enable button 2018-03-16 09:51:10 +01:00
b3yond c1a47473fe fixed bg image on some browsers. fixed invalid email error. 2018-02-17 15:51:33 +01:00
b3yond 4ff86b2510 fixed FPD for bot.html 2018-02-17 12:31:49 +01:00
b3yond ab9aa1070c This time I really fixed the css. almost. 2018-02-17 12:30:06 +01:00
b3yond 6c6be9d747 login check with cookies works now. fixed some layout stuff 2018-02-16 17:46:43 +01:00
b3yond 420866ac03 check if user already exists on registering 2018-02-16 14:16:50 +01:00
b3yond e056e80320 create db manually 2018-02-16 12:02:58 +01:00
b3yond e896f83729 It's unfortunate, but apparently it is impossible to keep this file in the repo. >.< 2018-02-16 11:59:58 +01:00
b3yond 8143842e3c readded db stub 2018-02-16 11:51:58 +01:00
b3yond 7b232725f7 delete db file 2018-02-16 11:50:23 +01:00
b3yond 32e15775de gitignore readd. how do I prevent ticketfrei.sqlite from updating the stub in the repo? 2018-02-16 11:46:10 +01:00
b3yond 02c1b1d0e5 fixed wallpaper after confirmation mail 2018-02-16 11:39:31 +01:00
b3yond f332c90207 confirmation emails work now, accounts can be created. 2018-02-16 11:33:27 +01:00
b3yond 7e7ed3f2ca Excepted IMAP connection Error 2018-01-30 16:10:33 +01:00
b3yond 4eeef55de4 small steps... 2018-01-26 17:54:11 +01:00
b3yond ebefa6f7e4 fix sqlite commands, rename to website.py 2018-01-26 15:19:03 +01:00
b3yond 71a02ecbc4 excepted IMAP4 error with unknown cause 2018-01-23 09:18:59 +01:00
b3yond fd96cbe6c2 excepted IMAP4 error with unknown cause 2018-01-23 09:17:26 +01:00
b3yond eb00b9dba4 summary: what is ticketfrei? 2018-01-19 16:42:43 +01:00
b3yond 728b191505 merged ticketfrei 1.0 into multi-deployment #3 2018-01-19 16:33:46 +01:00
b3yond 7e759143fe one \ to much lol 2018-01-19 16:27:30 +01:00
b3yond bc2b6aa828 updated README to version 1.0. you can disable accounts now 2018-01-19 16:00:36 +01:00
b3yond f5759ad60d excepted Mastodon API Error with a too broad exception 2018-01-19 00:17:09 +01:00
b3yond 45d5166499 excepted TweepError that was raised without an explanation further than 503 2018-01-18 21:48:36 +01:00
b3yond 0dc17d65f0 added more save_last(), schadet nicht 2018-01-18 20:15:41 +01:00
b3yond 38f7e31d6c bots don't own trigger anymore 2018-01-18 15:18:20 +01:00
b3yond 424352e83c twitter accidentially crawled too many tweets 2018-01-18 15:14:04 +01:00
b3yond 4b1848c895 better error handling of FileExistsError, fixed regex for mastobot 2018-01-18 15:10:05 +01:00
b3yond 1119d259f1 added regex magic so twitter & masto don't mention themselves by accident 2018-01-18 14:48:53 +01:00
b3yond 95466c5cc3 mailbot doesn't crawl mails which it wrote itself anymore 2018-01-18 14:23:11 +01:00
b3yond 331bf6277e bugfix: gave Report.__init__() twitter User object, not screen_name 2018-01-18 13:59:37 +01:00
b3yond 3638c36c29 twitterbot.crawl() returns reports now, not statuses 2018-01-18 13:54:32 +01:00
b3yond 5c56d97e23 only send one status at a time 2018-01-18 13:42:23 +01:00
b3yond 4d39fd861d bugfix: FileExistsError 2018-01-18 13:40:07 +01:00
b3yond 99a5c76e24 function needs to take an argument 2018-01-18 13:19:11 +01:00
b3yond 7e3e4cf706 changed ticketfrei flow logic, integrated mailbot!!! #11 2018-01-18 13:06:53 +01:00
b3yond 7129c50030 mailbot uses reports now, and doesn't need to own trigger 2018-01-18 12:42:37 +01:00
b3yond 42c72400fa Standardized reports; moved flow() logic to crawl(), repost(), & post(); bots don't own Trigger anymore 2018-01-18 11:41:08 +01:00
b3yond da559d6d8a Tried to make confirm link work (WIP) 2018-01-18 09:39:06 +01:00
b3yond 066fa32958 added nice slogan! 2018-01-09 23:01:01 +01:00
b3yond 1f77827f54 check hashes at login (not tested) 2018-01-09 23:00:00 +01:00
b3yond ee96441f21 generating confirmation links 2018-01-08 22:56:05 +01:00
b3yond 17c8febe49 first attempt at confirmation mails 2018-01-08 01:16:34 +01:00
b3yond b2e85881fc renamed promotion directory on master, too 2018-01-08 00:17:19 +01:00
b3yond e0b413d653 renamed promotion directory 2018-01-08 00:16:29 +01:00
b3yond c7f80e27a6 Merge branch 'master' of https://github.com/b3yond/ticketfrei into multi-deployment 2018-01-08 00:14:38 +01:00
b3yond 7f08bfe4e4 Started with the index page, worked on login & register. 2018-01-08 00:09:25 +01:00
Thomas L 93cb87fc81 replace logger class with standard python loggin 2018-01-07 20:22:32 +01:00
Thomas L a3c711ff8e remove our api-keys m( 2018-01-07 18:48:35 +01:00
b3yond 94016e8337 blacklisted certain racist slurs 2018-01-07 01:33:10 +01:00
b3yond 1ee68b19ac exchanged link & QR-Code 2018-01-07 00:52:03 +01:00
b3yond 3913f2c991 created a flyer for autonomous centers 2018-01-06 22:20:36 +01:00
b3yond 98fec6b640 wrote 3 articles for a false-flag-flyer :D 2018-01-06 21:31:23 +01:00
b3yond 19460158f3 wrote fully fleshed out mailbot. has to be connected to ticketfrei.py #11 2018-01-05 17:13:41 +01:00
b3yond 4b9ebdaad8 started an IMAP listener to implement a 3rd bot: the Mailbot. #11 2018-01-05 14:16:24 +01:00
b3yond 6cb30f36d6 attach logfiles to shutdown mails 2018-01-05 11:20:07 +01:00
b3yond bffbc2075b removed unused shutdown contact, renamed variable 2018-01-05 11:02:45 +01:00
b3yond 6ad47b6a20 log traceback of all unexpected Exceptions 2018-01-05 10:52:15 +01:00
b3yond b4ea602a76 documented log config 2018-01-05 10:43:38 +01:00
b3yond d5b2d2b13b reworked logger class - also handles bot crashes and tbs now. added configline for log directory. 2018-01-05 10:42:31 +01:00
b3yond e7d17a30e2 typo 2018-01-04 12:23:41 +01:00
b3yond b66c9862ec improved the traceback messages 2018-01-04 12:20:59 +01:00
b3yond acc80dbaa5 small fix 2018-01-04 11:05:36 +01:00
b3yond 7bc17ef95e crash reports are now sent via mail. documented config.toml.example 2018-01-04 11:02:42 +01:00
b3yond 456f8decf9 added class to write mails to users 2018-01-01 11:23:50 +01:00
b3yond 7c00640afa finished changes to class structure 2017-12-30 16:33:34 +01:00
b3yond 5f2fa46a47 typo 2017-12-30 16:23:53 +01:00
b3yond b13baa018e moved log to own class 2017-12-30 16:20:25 +01:00
Thomas L 90560d6fec fix fd mode 2017-12-30 11:31:16 +01:00
b3yond d188086fc0 Renamed config file to config.toml #6 2017-12-30 10:32:20 +01:00
b3yond 3471fa9dd7 optimized install docs 2017-12-30 01:21:57 +01:00
b3yond f14b2aab6b added documentation -> python3 #7 2017-12-30 01:17:13 +01:00
b3yond e9c231e501 changed ticketfrei.py to python3 #7 2017-12-30 01:15:22 +01:00
b3yond 29d35c8d15 changed twitterbot to python3 + tweepy #7 2017-12-30 01:11:28 +01:00
b3yond 02a14598e5 Merge branch 'master' of https://github.com/b3yond/ticketfrei 2017-12-10 20:20:39 +01:00
b3yond 8bab892c2e added todo 2017-12-10 20:20:30 +01:00
b3yond dbc829a416 wrote documentation 2017-11-28 15:11:09 +01:00
b3yond f259c9eccb updated gitignore 2017-11-24 18:16:38 +01:00
b3yond bff57e5a3c new image 2017-11-24 18:15:56 +01:00
b3yond a261c2bc59 Merge branch 'master' of https://github.com/b3yond/ticketfrei 2017-11-24 18:13:52 +01:00
b3yond 48383f1499 patc designed a more readable sticker :D 2017-11-01 23:10:40 +01:00
b3yond 57ca702854 added nbg_ticketfrei logo 2017-10-18 19:15:16 +02:00
b3yond ced30cecd2 would be a nice feature 2017-10-17 15:29:09 +02:00
b3yond 4a46251971 Merge branch 'master' of https://github.com/b3yond/ticketfrei 2017-10-17 15:27:56 +02:00
b3yond ee256af154 advice about using screen 2017-10-17 15:27:24 +02:00
b3yond aa2489c2c0 Merge branch 'master' of dl6tom.de:public/ticketfrei 2017-10-17 00:16:51 +02:00
b3yond e5800fb1d5 added another todo point 2017-10-17 00:15:56 +02:00
Thomas L 6a4136412c add license 2017-10-17 00:14:57 +02:00
b3yond 78f7fb550a added 2 todo points 2017-10-17 00:04:21 +02:00
ng0 2c7c6fb128 minor correction to ticketfrei.cfg.example 2017-10-14 19:54:57 +00:00
b3yond 50f81c3bc1 invented a campaign 2017-10-11 22:22:53 +02:00
78 changed files with 36269 additions and 674 deletions

10
.editorconfig Normal file
View 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
View 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.

View 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.

View 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.*

8
.gitignore vendored
View File

@ -1,12 +1,20 @@
*.swp
*.pyc
.idea/
__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/

15
LICENSE Normal file
View File

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

413
README.md
View File

@ -1,68 +1,379 @@
# Ticketfrei micro messaging bot
# Ticketfrei social bot
<!-- This mastodon/twitter bot has one purpose - breaking the law. -->
Ticketfrei is a mastodon/twitter/mail bot to dodge ticket controllers in public
transport systems.
The functionality is simple: it retweets every tweet where it is mentioned.
## Mission
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.
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.**
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.
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.
Website: https://wiki.links-it.de/IT/Ticketfrei
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!
# Install
## How It Works
Install python and virtualenv with your favourite package manager.
Create and activate virtualenv
The functionality is simple: It retweets every tweet where it is mentioned.
This leads to a community which evolves around it. If you see ticket
controllers, 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.
Today, you can use a Twitter, Mastodon, Telegram, and Mail with the account.
They will communicate with each other; if someone warns others via Mail,
Telegram, Twitter and Mastodon users will also see the message. And vice versa.
In version 2, this 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
$ virtualenv -p python2 .
$ . bin/activate
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 dependencies
```shell
$ pip install python-twitter pytoml requests Mastodon.py
```
Configure
```shell
$ cp ticketfrei.cfg.example ticketfrei.cfg
$ vim ticketfrei.cfg
```
Edit the account credentials, so your bot can use your accounts.
Also add the words to the goodlist, which you want to require. A tweet is only retweeted, if it contains at least one of them. If you want to RT everything, just add your account name.
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.
Note that atm the good- & blacklist are still outside of ticketfrei.cfg, in separate files. we will repare this soon.
To keep the bots running when you are logged out of the shell, you can use screen:
Install the necessary packages, create and activate virtualenv:
```shell
sudo apt-get install screen
screen
python ticketfrei.py
virtualenv -p python3 .
. bin/activate
```
## ideas
Install the dependencies:
* You can only use the twitter API if you have confirmed a phone number and sacrificed a penguin in a blood ritual. So we should build it in a way that it uses the twitter web GUI. It's difficult, but maybe it works. We had another twitter bot that worked similarly, years ago: https://github.com/b3yond/twitter-bot
* Build a tool that deletes wrong toots/tweets on both platforms, would work nicely with a web UI.
* write the muted people to the db, to easily undo the mutes if necessary.
```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx gitpython
```
Configure the bot:
```shell
cp config.toml.example config.toml
vim config.toml
```
This configuration is only for the admin. Moderators can log into
twitter/mastodon/mail and configure their personal bot on the settings page.
Set up LetsEncrypt:
```shell
sudo apt-get install python-certbot-nginx -t stretch-backports
sudo certbot --authenticator webroot --installer nginx --agree-tos --redirect --hsts
```
Configure exim4 for using mbox files.
```
sudo dpkg-reconfigure exim4-config
# Choose the following values:
# internet site; mail is sent and received directly using SMTP
# your domain name
#
# your domain name
#
#
# No
# mbox format in /var/mail/
# No
```
Deploy ticketfrei with uwsgi:
```shell
echo "Enter your domain name into the following prompt:" && read DOMAIN
# configure nginx
sudo sed -r "s/example.org/$DOMAIN/g" deployment/example.org.conf > /etc/nginx/sites-enabled/$DOMAIN.conf
# create folder for database
sudo mkdir /var/ticketfrei
sudo chown www-data:www-data -R /var/ticketfrei
# create folder for socket
sudo mkdir /var/run/ticketfrei
sudo chown -R www-data:www-data /var/run/ticketfrei
sudo -s
echo "mkdir /var/run/ticketfrei" >> /etc/rc.local
echo "chown -R www-data:www-data /var/run/ticketfrei" >> /etc/rc.local
echo "service ticketfrei-web restart" >> /etc/rc.local
exit
# change /etc/aliases permissions to be able to receive reports per mail
sudo chown root:www-data /etc/aliases
sudo chmod 664 /etc/aliases
# create folder for logs
sudo mkdir /var/log/ticketfrei
sudo chown www-data:www-data -R /var/log/ticketfrei
# start up nginx
sudo service nginx restart
# create and start the frontend systemd service
sudo cp deployment/ticketfrei-web.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl start ticketfrei-web.service
# create and start the backend systemd service
sudo cp deployment/ticketfrei-backend.service /etc/systemd/system
sudo systemctl daemon-reload
sudo systemctl start ticketfrei-backend.service
```
### Backup
For automated backups, you need to backup these files:
* `/var/ticketfrei/db.sqlite`
* `/srv/ticketfrei/config.toml`
* `/etc/aliases`
You can find an example how to do this with borgbackup in the deployment
folder. Adjust it to your needs.
### Logs
There are several logfiles which you can look at:
```
# for the uwsgi deployment:
less /var/log/ticketfrei/uwsgi.log
# for the backend:
less /var/log/ticketfrei/backend.log
# for the systemd service:
less /var/log/syslog
# for the nginx web server:
less /var/log/nginx/example.org_error.log
# for the mail server
less /var/log/exim4/mainlog
```
### Development Install
If you want to install it locally to develop on it, note that twitter and mail
will probably not work. You should test them on a server instead.
```shell
sudo apt install python3 virtualenv uwsgi uwsgi-plugin-python3 nginx git
sudo git clone https://github.com/b3yond/ticketfrei
cd ticketfrei
git checkout multi-deployment
```
Install the necessary packages, create and activate virtualenv:
```shell
virtualenv -p python3 .
. bin/activate
```
Install the dependencies:
```shell
pip install tweepy pytoml Mastodon.py bottle pyjwt pylibscrypt Markdown twx
```
Configure the bot:
```shell
cp config.toml.example config.toml
vim config.toml
```
This configuration is only for the admin. Users can log into
twitter/mastodon/mail and configure their personal bot on the settings page.
```shell
# create folder for socket & database
sudo mkdir /var/ticketfrei
sudo chown $USER:$USER -R /var/ticketfrei
# create folder for logs
sudo mkdir /var/log/ticketfrei
sudo chown $USER:$USER -R /var/log/ticketfrei
# start Ticketfrei
./frontend.py & ./backend.py &
```
# Project History
## Version 1
- more of less hacked together during a mate-fueled weekend
- backend-only, twitter & mastodon
- just a script, which crawled & retweeted tweets, if they match a whitelist & blocklist
- whitelist & blocklist were just 2 files
## Version 2
Reasons for the rewrite:
- user management: Users should be able to run a Ticketfrei bot in their city
- without needing a server, without needing command line skills
- more networks; not only Twitter & Mastodon, also Email & Telegram
2 processes: backend & frontend.
The two Processes talk via a database.
The two Processes have separate log files.
Both processes take some config values from config.toml.
### Backend
The Backend takes care of crawling & spreading the reports.
backend.py:
- main loop which does the crawling & posting.
- loops through all cities in the database
- per city it tries all of the networks/bots:
- per network/bot it runs the crawl()-function to ask the social network for new reports
- then it checks whether the report is appropriate
- if yes, it posts the report via all networks/bots, which belong to the city.
config.py: imports config values
- Imports values from config.toml
- If there is no config file it tries to use environment variables,
- Apart from that it uses the default values.
bot.py: bot parent Class
- just the absolute minimum what a bot needs to be able to do: crawl + post
- is never instantiated, only inherited from
report.py: report Class
- defines how reports are supposed to look like
active_bots/mailbot.py as an example for how a network/bot works
- crawl():
- mails arrive at an mbox file through exim4
- the bot checks whether they are new
- the bot generates a report object from the mail and returns it to the backend.py-loop
- post():
- asks the database for the list of mails which want to receive reports for this city
- sends the report.text to those mail addresses
### Frontend
the architecture of the frontend is loosely oriented off [Model View
Controller](https://blog.codinghorror.com/understanding-model-view-controller/).
user.py (Model)
- high-level interface to talk to the database
- database calls; almost all values in the database are specific to a city/user
- user.py is also a Class for frontend web authentication
- user.py keeps the user-id, through which the frontend tracks authentication
db.py (Model)
- DB-Layout; creates the database if it doesn't exist yet.
- holds some database calls which are not city-specific.
frontend.py (Controller): bottle web application
- handles POST/GET requests
- talks to the database through user.py
- everyone can look at the pages, and register
- but only authenticated users can login and change settings
session.py: User Authentication
- takes care of session cookies and "403 unauthenticated" error messages
sendmail.py: helper script to send mails
- sends all mails the frontend, backend, and bots need to send
static/
- css, images, javascript for the login form etc.
template/ (view)
- base for the HTML generation, uses the bottle-template-framework
- wrapper.tpl is the base template for every other template
### active_bots: how to implement a new network
If you want to write a new bot, e.g. a Wire-Bot, you have to take these steps:
- look for a python-library which can talk to Wire
- the city/users have to provide authentication details; this needs a form in
the settings
- depending on the network either a password, a token, or an implementation
of the OAuth login flow
- the backend needs to crawl messages from the network, & post reports to the
network
Files you need to change:
1. active_bots/wire.py - crawl & post functions
2. settings.tpl - form to authenticate to the network & possible network specific settings.
3. frontend.py - routes for the forms you added to settings.tpl
4. db.py - database layout, to store the account credentials/tokens, and to save which message you have last seen
5. user.py - database calls to get or set values
## to do
Desktop/pycharm-community-2017.1.4/bin/pycharm.sh
- [x] Twitter: Crawl mentions
- [x] Mastodon: Crawl mentions
- [ ] Write toots/tweets to database/log
- [x] Twitter: retweet people
- [x] Mastodon: boost people
- [x] Twitter: access the API
- [ ] Web UI that lets you easily delete toots/tweets per db id and mute the tweet author
- [x] Write Bots as Classes to be easier implemented
- [x] Create extra Class for the filter
- [x] Put as much as possible into ticketfrei.cfg
- [x] Make both bots run on their own *and* next to each other
- [x] implement trigger class in retootbot
- [x] read config in retweetbot
- [x] put shutdown contact in ticketfrei.cfg

14
active_bots/__init__.py Normal file
View 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)

84
active_bots/mailbot.py Normal file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env python3
import logging
from sendmail import sendmail
import datetime
import mailbox
import email
import report
from bot import Bot
from config import config
from db import db
logger = logging.getLogger("main")
class Mailbot(Bot):
# returns a list of Report objects
def crawl(self, user):
reports = []
# todo: adjust to actual mailbox
try:
mails = mailbox.mbox("/var/mail/" + config['mail']['mbox_user'])
except FileNotFoundError:
logger.error("No mbox file found.")
return reports
for msg in mails:
if get_date_from_header(msg['Date']) > user.get_seen_mail():
if user.get_city().lower() in msg['To'].lower():
reports.append(make_report(msg, user))
return reports
# post/boost Report object
def post(self, user, report):
recipients = user.get_mailinglist()
for rec in recipients:
rec = rec[0]
unsubscribe_text = "\n_______\nYou don't want to receive those messages? Unsubscribe with this link: "
body = report.text + unsubscribe_text + config['web']['host'] + "/city/mail/unsubscribe/" \
+ db.mail_subscription_token(rec, user.get_city())
if rec not in report.author:
try:
city = user.get_city()
sendmail(rec, "Ticketfrei " + city + " Report",
city=city, body=body)
except Exception:
logger.error("Sending Mail failed.", exc_info=True)
def make_report(msg, user):
"""
generates a report out of a mail
:param msg: email.parser.Message object
:return: post: report.Report object
"""
# get a comparable date out of the email
date = get_date_from_header(msg['Date'])
author = msg['From'] # get mail author from email header
for part in msg.walk():
if part.get_content_type() == 'text/plain':
text = part.get_payload()
elif part.get_content_type() == 'text/html':
text = re.sub(r'<[^>]*>', '', msg.get_payload())
try:
post = report.Report(author, "mail", text, None, date)
except UnboundLocalError:
logger.error('No suitable message body')
return
user.save_seen_mail(date)
return post
def get_date_from_header(header):
"""
:param header: msg['Date']
:return: float: total seconds
"""
date_tuple = email.utils.parsedate_tz(header)
date_tuple = datetime.datetime.fromtimestamp(
email.utils.mktime_tz(date_tuple)
)
return (date_tuple - datetime.datetime(1970, 1, 1)).total_seconds()

124
active_bots/mastodonbot.py Executable file
View File

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

View File

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

90
active_bots/twitterbot.py Executable file
View 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("main")
class TwitterBot(Bot):
def get_api(self, user):
keys = user.get_twitter_credentials()
auth = tweepy.OAuthHandler(consumer_key=keys[0],
consumer_secret=keys[1])
auth.set_access_token(keys[2], # access_token_key
keys[3]) # access_token_secret
return tweepy.API(auth, wait_on_rate_limit=True)
def crawl(self, user):
"""
crawls all Tweets which mention the bot from the twitter rest API.
:return: reports: (list of report.Report objects)
"""
reports = []
try:
if user.get_last_twitter_request() + 60 > time():
return reports
except TypeError:
user.set_last_twitter_request(time())
try:
api = self.get_api(user)
except TypeError:
# When there is no twitter account for this bot, we want to
# seamlessly continue.
#logger.error("Error Authenticating Twitter", exc_info=True)
return reports
last_mention = user.get_seen_tweet()
try:
if last_mention == 0:
mentions = api.mentions_timeline()
else:
mentions = api.mentions_timeline(since_id=last_mention)
user.set_last_twitter_request(time())
for status in mentions:
if status._json['in_reply_to_status_id'] == None:
text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
"", status.text)
reports.append(report.Report(status.author.screen_name,
self,
text,
status.id,
status.created_at))
user.save_seen_tweet(status.id)
return reports
except tweepy.RateLimitError:
logger.error("Twitter API Error: Rate Limit Exceeded",
exc_info=True)
# :todo implement rate limiting
except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection", exc_info=True)
except tweepy.TweepError:
logger.error("Twitter API Error: General Error. User: " + str(user.uid), exc_info=True)
return []
def post(self, user, report):
try:
api = self.get_api(user)
except TypeError:
return # no twitter account for this user.
try:
if report.source == self:
api.retweet(report.id)
else:
text = report.text
if len(text) > 280:
text = text[:280 - 4] + u' ...'
api.update_status(status=text)
except requests.exceptions.ConnectionError:
logger.error("Twitter API Error: Bad Connection",
exc_info=True)
except tweepy.error.TweepError:
logger.error("Twitter API Error", exc_info=True)

2
appkeys/.gitignore vendored
View File

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

49
backend.py Executable file
View File

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

View File

@ -1,10 +0,0 @@
bastard
bitch
whore
hitler
slut
hure
jude
schwuchtel
fag
faggot

9
bot.py Normal file
View 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
View 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]))

16
config.toml.example Normal file
View File

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

319
db.py Normal file
View File

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

15
deployment/backend_daemon Normal file
View File

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

15
deployment/borgbackup.sh Normal file
View File

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

View File

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

11
deployment/uwsgi.ini Normal file
View 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
View 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

View File

@ -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?

View File

@ -1,16 +0,0 @@
# 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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!

1
local

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

4
logs/.gitignore vendored
View File

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

26
report.py Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
class Report(object):
"""
A ticketfrei report object.
Toots, Tweets, and E-Mails can be formed into ticketfrei reports.
"""
def __init__(self, author, source, text, id, timestamp):
"""
Constructor of a ticketfrei report
:param author: username of the author
:param source: mastodon, twitter, or email 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.source = source
self.text = text
self.timestamp = timestamp
self.id = id

9
requirements.txt Normal file
View File

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

View File

@ -1,122 +0,0 @@
#!/usr/bin/env python3
import pytoml as toml
import mastodon
import os
import pickle
import re
import time
import datetime
import trigger
import traceback
class RetootBot(object):
def __init__(self, config, filter, logpath=None):
self.config = config
self.filter = filter
self.register()
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()
if logpath:
self.logpath = logpath
else:
self.logpath = os.path.join("logs", str(datetime.datetime.now()))
def log(self, message, tb=False):
"""
Writing an error message to a logfile in logs/ and prints it.
:param message(string): Log message to be displayed
:param tb: String of the Traceback
"""
time = str(datetime.datetime.now())
if tb:
message = message + " The traceback is located at " + os.path.join("logs" + time)
with open(os.path.join("logs", time), 'w+') as f:
f.write(tb)
line = "[" + time + "] "+ message + "\n"
with open(self.logpath, 'a') as f:
try:
f.write(line)
except UnicodeEncodeError:
self.log("Failed to save log message due to UTF-8 error. ")
traceback.print_exc()
print line,
def register(self):
self.client_id = os.path.join(
'appkeys',
self.config['mapp']['name'] +
'@' + self.config['muser']['server']
)
if not os.path.isfile(self.client_id):
mastodon.Mastodon.create_app(
self.config['mapp']['name'],
api_base_url=self.config['muser']['server'],
to_file=self.client_id
)
def login(self):
self.m = mastodon.Mastodon(
client_id=self.client_id,
api_base_url=self.config['muser']['server']
)
self.m.log_in(
self.config['muser']['email'],
self.config['muser']['password']
)
def retoot(self, toots=()):
# toot external provided messages
for toot in toots:
self.m.toot(toot)
# boost mentions
retoots = []
for notification in self.m.notifications():
if (notification['type'] == 'mention'
and notification['status']['id'] not in self.seen_toots):
self.seen_toots.add(notification['status']['id'])
text_content = re.sub(r'<[^>]*>', '',
notification['status']['content'])
if not self.filter.is_ok(text_content):
continue
self.log('Boosting toot from %s: %s' % (
#notification['status']['id'],
notification['status']['account']['acct'],
notification['status']['content']))
self.m.status_reblog(notification['status']['id'])
retoots.append('%s: %s' % (
notification['status']['account']['acct'],
re.sub(r'@\S*', '', text_content)))
# If the Mastodon instance returns interesting Errors, add them here:
# save state
with os.fdopen(os.open('seen_toots.pickle.part', os.O_WRONLY | os.O_EXCL | os.O_CREAT), 'w') as f:
pickle.dump(self.seen_toots, f)
os.rename('seen_toots.pickle.part', 'seen_toots.pickle')
# return mentions for mirroring
return retoots
if __name__ == '__main__':
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('ticketfrei.cfg') as configfile:
config = toml.load(configfile)
filter = trigger.Trigger(config)
bot = RetootBot(config, filter)
while True:
bot.retoot()
time.sleep(1)

View File

@ -1,270 +0,0 @@
#!/usr/bin/env python
import twitter
import os
import datetime
import requests
import pytoml as toml
import trigger
from time import sleep
import traceback
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
triggers: a list of words, one of them has to be in a tweet for it to be
retweeted
last_mention: the ID of the last tweet which mentioned you
"""
def __init__(self, trigger, config, historypath="last_mention", logpath=None):
"""
Initializes the bot and loads all the necessary data.
:param historypath: Path to the file with ID of the last retweeted
Tweet
:param logpath: Path to the file where the log is stored
"""
self.config = config
keys = self.get_api_keys()
self.api = twitter.Api(consumer_key=keys[0],
consumer_secret=keys[1],
access_token_key=keys[2],
access_token_secret=keys[3])
self.historypath = historypath
try:
self.no_shutdown_contact = False
self.user_id = self.config['tapp']['shutdown_contact_userid']
self.screen_name = \
self.config['tapp']['shutdown_contact_screen_name']
except KeyError:
self.no_shutdown_contact = True
self.last_mention = self.get_history(self.historypath)
self.trigger = trigger
self.waitcounter = 0
if logpath:
self.logpath = logpath
else:
self.logpath = os.path.join("logs", str(datetime.datetime.now()))
print "Path of logfile: " + self.logpath
def get_api_keys(self):
"""
How to get these keys is described in doc/twitter_api.md
After you received keys, store them in your ticketfrei.cfg like this:
[tapp]
consumer_key = "..."
consumer_secret = "..."
[tuser]
access_token_key = "..."
access_token_secret = "..."
:return: keys: list of these 4 strings.
"""
keys = []
keys.append(self.config['tapp']['consumer_key'])
keys.append(self.config['tapp']['consumer_secret'])
keys.append(self.config['tuser']['access_token_key'])
keys.append(self.config['tuser']['access_token_secret'])
return keys
def log(self, message, tb=False):
"""
Writing an error message to a logfile in logs/ and prints it.
:param message(string): Log message to be displayed
:param tb: String of the Traceback
"""
time = str(datetime.datetime.now())
if tb:
message = message + " The traceback is located at " + os.path.join("logs" + time)
with open(os.path.join("logs", time), 'w+') as f:
f.write(tb)
line = "[" + time + "] "+ message + "\n"
with open(self.logpath, 'a') as f:
try:
f.write(line)
except UnicodeEncodeError:
self.log("Failed to save log message due to UTF-8 error. ")
traceback.print_exc()
print line,
def get_history(self, path):
""" This counter is needed to keep track of your mentions, so you
don't double RT them
: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_mention(self):
""" Saves the last retweeted tweet in last_mention. """
with open(self.historypath, "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 format_mastodon(self, status):
"""
Bridge your Retweets to mastodon.
:todo vmann: add all the mastodon API magic.
:param status: Object of a tweet.
:return: toot: text tooted on mastodon, e.g. "_b3yond: There are
uniformed controllers in the U2 at Opernhaus."
"""
toot = status.user.name + ": " + status.text
return toot
def crawl_mentions(self):
"""
crawls all Tweets which mention the bot from the twitter rest API.
:return: list of Status objects
"""
try:
if not self.waiting():
mentions = self.api.GetMentions(since_id=self.last_mention)
return mentions
except twitter.TwitterError:
self.log("Twitter API Error: Rate Limit Exceeded.")
self.waitcounter += 60*15 + 1
except requests.exceptions.ConnectionError:
self.log("Twitter API Error: Bad Connection.")
self.waitcounter += 10
return None
def retweet(self, status):
"""
Retweets a given tweet.
:param status: A tweet object.
:return: toot: string of the tweet, to toot on mastodon.
"""
while 1:
try:
self.api.PostRetweet(status.id)
self.log("Retweeted: " + self.format_mastodon(status))
if status.id > self.last_mention:
self.last_mention = status.id
return self.format_mastodon(status)
# maybe one day we get rid of this error. If not, try to uncomment
# these lines.
except twitter.error.TwitterError:
self.log("Twitter API Error: You probably already retweeted this tweet: " + status.text)
if status.id > self.last_mention:
self.last_mention = status.id
return None
except requests.exceptions.ConnectionError:
self.log("Twitter API Error: Bad Connection.")
sleep(10)
def tweet(self, post):
"""
Tweet a post.
:param post: String with the text to tweet.
"""
if len(post) > 280:
post = post[:280 - 4] + u' ...'
while 1:
try:
self.api.PostUpdate(status=post)
return
except requests.exceptions.ConnectionError:
self.log("Twitter API Error: Bad Connection.")
sleep(10)
def flow(self, to_tweet=()):
""" The flow of crawling mentions and retweeting them.
:param to_tweet: list of strings to tweet
:return list of retweeted tweets, to toot on mastodon
"""
# Tweet the toots the Retootbot gives to us
for post in to_tweet:
self.tweet(post)
# Store all mentions in a list of Status Objects
mentions = self.crawl_mentions()
mastodon = []
if mentions is not None:
for status in mentions:
# Is the Text of the Tweet in the triggerlist?
if self.trigger.is_ok(status.text):
# Retweet status
toot = self.retweet(status)
if toot:
mastodon.append(toot)
# save the id so it doesn't get crawled again
if status.id > self.last_mention:
self.last_mention = status.id
self.save_last_mention()
# Return Retweets for tooting on mastodon
return mastodon
def shutdown(self):
""" If something breaks, it shuts down the bot and messages the owner.
"""
logmessage = "Shit went wrong, closing down."
if self.screen_name:
logmessage = logmessage + " Sending message to " + self.screen_name
self.log(logmessage)
if self.no_shutdown_contact:
return
self.save_last_mention()
try:
self.api.PostDirectMessage("Help! I broke down. restart me pls :$",
self.user_id, self.screen_name)
except:
traceback.print_exc()
print
if __name__ == "__main__":
# create an Api object
with open('ticketfrei.cfg') as configfile:
config = toml.load(configfile)
trigger = trigger.Trigger(config)
bot = RetweetBot(trigger, config)
try:
while True:
bot.flow()
sleep(60)
except KeyboardInterrupt:
print "Good bye! Remember to restart the bot."
except:
traceback.print_exc()
print
bot.shutdown()

31
sendmail.py Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
from config import config
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
from getpass import getuser
import smtplib
from socket import getfqdn
logger = logging.getLogger("main")
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))
with smtplib.SMTP('localhost') as smtp:
smtp.send_message(msg)
# For testing:
if __name__ == '__main__':
sendmail(config['web']['contact'], "Test Mail",
body="This is a test mail.")

32
session.py Normal file
View 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
View 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
View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

82
static/index.html Normal file
View 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>
&nbsp;
<h2>Our Mission</h2>
<p>Contrary to popular belief, Lorem Ipsum is not simply random
text. It has roots in a piece of classical Latin literature
from 45 BC, making it over 2000 years old. Richard
McClintock, a Latin professor at Hampden-Sydney College in
Virginia, looked up one of the more obscure Latin words,
consectetur, from a Lorem Ipsum passage, and going through
the cites of the word in classical literature, discovered
the undoubtable source. Lorem Ipsum comes from sections
1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum"
(The Extremes of Good and Evil) by Cicero, written in 45
BC. This book is a treatise on the theory of ethics, very
popular during the Renaissance. The first line of Lorem
Ipsum, "Lorem ipsum dolor sit amet..", comes from a line
in section 1.10.32.</p>
<br>
</div>
<div class=footer>
Contribute on <a href="https://github.com/b3yond/ticketfrei">GitHub!</a>
</div>
</div>
</body>

View File

@ -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>

View 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.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

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

1312
static/jquery-ui-1.12.1/jquery-ui.css vendored Normal file

File diff suppressed because it is too large Load Diff

18706
static/jquery-ui-1.12.1/jquery-ui.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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;
}

File diff suppressed because one or more lines are too long

View 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;
}

File diff suppressed because one or more lines are too long

View 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
View 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

File diff suppressed because one or more lines are too long

38
static/register.html Normal file
View 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
View 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
View 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
View File

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

16
template/mail.tpl Normal file
View 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
View 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>

View 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
View 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
View 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
View 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>

View File

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

View File

@ -1,36 +0,0 @@
#!/usr/bin/env python
import pytoml as toml
import time
import traceback
import os
import datetime
from retootbot import RetootBot
from retweetbot import RetweetBot
from trigger import Trigger
if __name__ == '__main__':
# read config in TOML format (https://github.com/toml-lang/toml#toml)
with open('ticketfrei.cfg') as configfile:
config = toml.load(configfile)
trigger = Trigger(config)
logpath = os.path.join("logs", str(datetime.datetime.now()))
mbot = RetootBot(config, trigger, logpath=logpath)
tbot = RetweetBot(trigger, config, logpath=logpath)
try:
statuses = []
while True:
statuses = mbot.retoot(statuses)
statuses = tbot.flow(statuses)
time.sleep(60)
except KeyboardInterrupt:
print "Good bye. Remember to restart the bot!"
except:
traceback.print_exc()
tbot.shutdown()

View File

@ -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("ticketfrei.cfg", "r") as configfile:
config = toml.load(configfile)
print("testing the trigger")
trigger = Trigger(config)
print("Printing words which trigger the bot:")
for i in trigger.goodlist:
print(i)
print()
print("Printing words which block a bot:")
for i in trigger.blacklist:
print(i)

476
user.py Normal file
View File

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

6
wsgi.py Normal file
View File

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