Compare commits
426 commits
master
...
refactorin
Author | SHA1 | Date | |
---|---|---|---|
b3yond | 833be826e9 | ||
7ef8c3e197 | |||
768a4350bd | |||
9906346691 | |||
f5f741a5c2 | |||
eabc43cdba | |||
e4d0fc3b89 | |||
5c844157ce | |||
050e641bb0 | |||
fa3dda1c60 | |||
5535d3535f | |||
668157c552 | |||
b3yond | 39cf3bd070 | ||
b3yond | 382532cf5c | ||
b3yond | cfb9eabee9 | ||
b3yond | a27d47eb8b | ||
b3yond | e34944fcaa | ||
b3yond | d6db1879f9 | ||
b3yond | 02f117a864 | ||
b3yond | 482350f8c7 | ||
b3yond | 6b52a6303a | ||
b3yond | 2a90573d5e | ||
b3yond | e735936c7a | ||
b3yond | ee9b051c71 | ||
b3yond | 139195fd02 | ||
b3yond | 3dd976ef40 | ||
b3yond | cdecd170a0 | ||
b3yond | ec68f17b32 | ||
b3yond | ddefc2aafa | ||
b3yond | 60e1d8ec30 | ||
b3yond | 4882930516 | ||
b3yond | d5b0ba9b6d | ||
b3yond | 26fa98ad9b | ||
b3yond | de525adb7a | ||
b3yond | 30c49bbfc8 | ||
b3yond | 880b327b20 | ||
b3yond | 467fdaa42a | ||
b3yond | a4996266a1 | ||
b3yond | c9c153117e | ||
b3yond | 54489807da | ||
b3yond | 4b8798ddea | ||
6a5e7f5028 | |||
7507d0392d | |||
b3yond | 4bd99ebb90 | ||
b3yond | 12a0b1efe5 | ||
b3yond | a38c2316f2 | ||
b3yond | 76b3b574f0 | ||
b3yond | 2ce27fc52f | ||
b3yond | 1c8853341a | ||
b3yond | a529f4eb23 | ||
b3yond | 521f0e7ef2 | ||
2bee67bf84 | |||
cb2f3cb2e1 | |||
a47ad74619 | |||
b3yond | f6c19abad6 | ||
b3yond | e7e230b2f0 | ||
b3yond | e72d4872c0 | ||
b3yond | d5823ee1ad | ||
b3yond | 268b9748c3 | ||
b3yond | 8e1234d9b5 | ||
b3yond | 4c61b1ba99 | ||
b3yond | 5a4763366b | ||
b3yond | 945a90c7e1 | ||
b3yond | bc7a4a72f8 | ||
b3yond | d964927a3f | ||
b3yond | 238dd20d20 | ||
b3yond | f274d25822 | ||
b3yond | 710a89c282 | ||
b3yond | 8b36589557 | ||
b3yond | 7cb211b4cb | ||
9508618347 | |||
b3yond | 651e684316 | ||
b3yond | 1a0ae78ac1 | ||
b3yond | 01f33ea29a | ||
b3yond | 400e15d18a | ||
b3yond | 55db252f44 | ||
b3yond | f64142d882 | ||
b3yond | f286c127ba | ||
b3yond | 4428fa932f | ||
b3yond | cc5ab22be5 | ||
b3yond | 56e948b798 | ||
b3yond | c36b8ab673 | ||
b3yond | 17df4f15e4 | ||
b3yond | b5de7cde9f | ||
b3yond | 8eb2d98c03 | ||
b3yond | 9836ec7752 | ||
b3yond | 9e8cfa624c | ||
b3yond | 084049bbfe | ||
b3yond | 6a8cf5c6af | ||
b3yond | de657ba350 | ||
b3yond | bbe27e2586 | ||
b3yond | 9a3c09b119 | ||
b3yond | 30de2196ac | ||
b3yond | 9ca521493a | ||
b3yond | 0449d892a3 | ||
b3yond | f59be986e2 | ||
b3yond | 79d5a6f112 | ||
b3yond | 9800b52153 | ||
b3yond | 54930a32f6 | ||
b3yond | 0b862e35c8 | ||
b3yond | 13fcb41148 | ||
b3yond | d3d7bd098d | ||
b3yond | 8a4cc17575 | ||
b3yond | 3d669e6caf | ||
b3yond | e032ecbcc3 | ||
b3yond | 30f1f8a21c | ||
b3yond | 942f19fefe | ||
b3yond | 5119c6bfbb | ||
b3yond | 2068b99b87 | ||
b3yond | 4851fc0b63 | ||
b3yond | b9a4899981 | ||
b3yond | 304d83ffad | ||
b3yond | 9b3efd7bd2 | ||
b3yond | 24598f0b87 | ||
b3yond | dc8f51c632 | ||
b3yond | 34d43f1911 | ||
b3yond | 2a4d517f1d | ||
b3yond | 2d12aa7107 | ||
b3yond | 47a7452eb4 | ||
b3yond | 234ed59049 | ||
b3yond | 5efea773b8 | ||
b3yond | 3b19278774 | ||
b3yond | 03033d26d7 | ||
b3yond | b35e885ae2 | ||
b3yond | d61d5750bb | ||
b3yond | aa9267e8d1 | ||
b3yond | 732ac1c5d3 | ||
b3yond | b94ead7041 | ||
b3yond | b20a080129 | ||
b3yond | 1412dbc54c | ||
b3yond | f28df3ce3e | ||
b3yond | 0cf1d8b603 | ||
b3yond | 244bde51b6 | ||
b3yond | 36c21dbfbb | ||
b3yond | f360c4f8fd | ||
b3yond | fcab07246b | ||
b3yond | 11f3c5713b | ||
b3yond | 1a793657af | ||
b3yond | e9ac7286d9 | ||
b3yond | 823df7b04a | ||
1703eb3802 | |||
72d6798022 | |||
c2ed73bafc | |||
c576888da5 | |||
b3yond | 25bfe8e838 | ||
b3yond | eae077cb9b | ||
b3yond | 91181e1cf8 | ||
b3yond | 6757e62242 | ||
b3yond | c37a447392 | ||
b3yond | 3e83ba95da | ||
b3yond | faaf8ac5f4 | ||
b3yond | a54538bcea | ||
b3yond | 6f3c953736 | ||
b3yond | 0624bcb378 | ||
b3yond | 6cac81e444 | ||
f68a869309 | |||
b3yond | a0bd5e69e1 | ||
b3yond | 4b953f54e5 | ||
8acbfb4569 | |||
b3yond | 439dbeb1fa | ||
b3yond | fd8b29c55f | ||
b3yond | f1d7215eba | ||
b3yond | 4586e14ee4 | ||
b3yond | 40c834020a | ||
b85360b0a8 | |||
185014a452 | |||
b3yond | b5f6854a1c | ||
b3yond | 7ca904564c | ||
b3yond | b80b80dc43 | ||
b3yond | 96329e968e | ||
b3yond | c7aa87cb3b | ||
b3yond | ad4e65e0fa | ||
b3yond | 9c599cec37 | ||
b3yond | 848b7b1cb5 | ||
0ffe4daac8 | |||
b3yond | 372e0612a6 | ||
b3yond | 57a2e4dcb1 | ||
b3yond | ec399db2eb | ||
b3yond | ef0ce8f9f1 | ||
b3yond | 27b63d9f8f | ||
b3yond | cc0b3378a9 | ||
b3yond | d002969377 | ||
b3yond | 1f0583da74 | ||
b3yond | e1eb737ad0 | ||
b3yond | 9beb864a2f | ||
b3yond | a05205289f | ||
b3yond | 89fce872f3 | ||
b3yond | d7eba3d233 | ||
b3yond | 10b3550ad6 | ||
b3yond | 2d879383d4 | ||
b3yond | 4343be7e06 | ||
b3yond | 55a804f0d6 | ||
b3yond | cae74a5715 | ||
b3yond | 3d23b47a6e | ||
57a4a50254 | |||
a8504971ea | |||
b3yond | 9db71e485d | ||
b3yond | 8dfffffe76 | ||
b3yond | 4fb2930c6c | ||
b3yond | fd8f236cdd | ||
b3yond | 44cd1308ba | ||
b3yond | 4c6ab2d3ae | ||
b3yond | 0719b094f8 | ||
a48ba9ebf8 | |||
4b37c0df3d | |||
b3yond | 83d8700e30 | ||
86d63fe9a0 | |||
b3yond | bfd9a2d5fe | ||
b3yond | 7543bf3e6e | ||
b3yond | cd5eeb3917 | ||
b3yond | f4736c91dd | ||
b3yond | 559b709b8f | ||
b3yond | 628fcb4f95 | ||
b3yond | 2a9c5c657f | ||
b3yond | c9dfb6611a | ||
b3yond | 04a6b82c1b | ||
29a577508f | |||
b3yond | d4d58daf40 | ||
b3yond | 48d44cf698 | ||
b3yond | 9274dfdecb | ||
b3yond | 5ec4d1aab0 | ||
b3yond | 62eb588b28 | ||
b3yond | 9885e39d68 | ||
b3yond | 01b3657c8e | ||
6996cbfc09 | |||
b3yond | 591020f8cc | ||
b3yond | d706c4f1cc | ||
642cf429e5 | |||
b3yond | dd24a2b265 | ||
b3yond | 3afa73ccaf | ||
b3yond | 1a76cba4fb | ||
b3yond | 84746a6d01 | ||
b3yond | 064ca181c0 | ||
b3yond | 16580f3181 | ||
b3yond | 7f8697947c | ||
b3yond | 758ff1db46 | ||
b3yond | 9b01ac7eac | ||
b3yond | 898f229145 | ||
b3yond | 0b41b43421 | ||
b3yond | 24beedf467 | ||
fd2a389d12 | |||
b3yond | 25c57039ea | ||
b3yond | 20cfe159e9 | ||
57cf3bd7d6 | |||
1af14a5db4 | |||
22de5e7e4e | |||
b3yond | 4d556ec595 | ||
b3yond | bf7c21c113 | ||
b3yond | 45d4cd2062 | ||
b3yond | 034513718f | ||
b3yond | 261496c097 | ||
b3yond | 19cc64d00d | ||
b3yond | 27497e7129 | ||
d280130b29 | |||
78331212e6 | |||
b3yond | 10fb150c21 | ||
b3yond | 29c35be8a5 | ||
bfc311b6c9 | |||
4981223ee8 | |||
9339015101 | |||
4850860f82 | |||
b3yond | c9fd91de74 | ||
b3yond | affd209a3b | ||
b3yond | ca55223be9 | ||
b3yond | 5670c92d33 | ||
2b6b3a2263 | |||
b3yond | 0aa1d79621 | ||
b3yond | 788f55860b | ||
b3yond | ba6e13a2be | ||
8e08eb9c2e | |||
b3yond | c71bc8574a | ||
b3yond | bc41d7460c | ||
b3yond | 9425fde917 | ||
88afab1270 | |||
5db529702c | |||
b3yond | 66bb1f86a3 | ||
b3yond | 49bd00fba3 | ||
b3yond | ec3053a0ab | ||
036c742f34 | |||
1dd75c10d5 | |||
890e720c91 | |||
a3e33c36c6 | |||
670a1a6d8f | |||
b3yond | 51dec7e072 | ||
c3f9f86d3f | |||
b3yond | 2d7b222c21 | ||
dde4e6af7b | |||
cb764f2ec3 | |||
b3yond | d207d4e960 | ||
b3yond | 5d2ffbd935 | ||
ce79b37b38 | |||
2fdc6f1f28 | |||
f99b44d815 | |||
c980e7abb5 | |||
b3yond | 95ada7ba62 | ||
b3yond | 9ac7ab3b70 | ||
b3yond | 64f1fff275 | ||
3ea06d1e93 | |||
daf6fe831f | |||
751f9154cc | |||
b3yond | 061fb62bdc | ||
305fb8e06a | |||
b3yond | ba9b28f254 | ||
b3yond | a8efcd7825 | ||
b3yond | 17d044ec20 | ||
b3yond | be118fb4bd | ||
b3yond | aa5669b019 | ||
b3yond | 5f55eb88ff | ||
b3yond | c548a81272 | ||
b3yond | a65d410e4f | ||
b3yond | 570792ba37 | ||
b3yond | c612a9dee0 | ||
b3yond | bd2599c91a | ||
b3yond | 81e2357e2f | ||
b3yond | a3b74dcfff | ||
b3yond | 32e86a3c0e | ||
b3yond | b9613a60de | ||
404be47d1b | |||
b3yond | 235b8524f8 | ||
b3yond | 9e09dcea84 | ||
b3yond | c48704ea73 | ||
b3yond | cdc88e3ee3 | ||
b3yond | ee8040893e | ||
b3yond | 390f4dc76e | ||
b3yond | a176f856d8 | ||
b3yond | 529270a396 | ||
3f4ec83abe | |||
b3yond | f9033a009f | ||
b3yond | eb0252f235 | ||
b3yond | 9cc2bf4228 | ||
b3yond | 28891d5069 | ||
b3yond | 9e70ff6866 | ||
b3yond | 87302faf9e | ||
b3yond | 8a7c2f0110 | ||
b3yond | 7bbcbe1ab1 | ||
b3yond | 4b21dddddf | ||
b3yond | 26d1282413 | ||
b3yond | 79f301d823 | ||
b3yond | 2ce2a45f7b | ||
b3yond | 52c2d1e341 | ||
b3yond | 1f01938a8c | ||
b3yond | a7bae0aed9 | ||
b3yond | 1b75e03fc5 | ||
b3yond | 7ccf6917c8 | ||
b3yond | 2e89f9bf2d | ||
b3yond | fb36221a40 | ||
b3yond | 0c04ce4b70 | ||
b3yond | 1e0a8a09ed | ||
b3yond | 27902954e8 | ||
b3yond | 821f201454 | ||
b3yond | 9e221ed290 | ||
b3yond | 3bc1010edf | ||
b3yond | 0ba2438541 | ||
b3yond | 0acb89ebf0 | ||
b3yond | c9d5f7441a | ||
b3yond | ace28ee25a | ||
b3yond | 63cf134ffa | ||
b3yond | c0328be3a4 | ||
b3yond | c9e6a35372 | ||
b3yond | 04e05ee8ca | ||
b3yond | 37b2706a3b | ||
b3yond | 9305a32eb7 | ||
b3yond | 12fbbde79c | ||
b3yond | 21e4af6fa9 | ||
b3yond | c8e67d1937 | ||
b3yond | 10de40549c | ||
b3yond | ee61ba19e6 | ||
b3yond | 72d0acb20a | ||
b3yond | b174db3cfe | ||
b3yond | 048bad181b | ||
b3yond | b5288f341c | ||
b3yond | 75e1ff902c | ||
b3yond | d6a0c6d377 | ||
b3yond | cde5494de3 | ||
b3yond | ff73c5dc21 | ||
b3yond | 9ef0b27970 | ||
b3yond | 9f060b405e | ||
b3yond | 5feb6cf5be | ||
b3yond | da421769e9 | ||
b3yond | 89ce129b38 | ||
b3yond | 2f74791dd6 | ||
b3yond | 2e80d10222 | ||
b3yond | 2b4d8650c9 | ||
b3yond | 7689eb25f8 | ||
f2a0cf18b4 | |||
f0aaa4dc54 | |||
b3yond | b9e1b38963 | ||
b3yond | 409f9e80f8 | ||
b3yond | 851992803f | ||
b3yond | 79a8965d1c | ||
b3yond | a0ca940008 | ||
b3yond | 5c98aa7677 | ||
b3yond | 01ad0e1c40 | ||
b3yond | e962bbbe85 | ||
b3yond | 0b89a52da3 | ||
b3yond | 31a54fc19f | ||
b3yond | 654af44534 | ||
b3yond | 8357be7f7d | ||
b3yond | 98dd5e4212 | ||
b3yond | aa45a8e814 | ||
b3yond | 0f6fc60b5e | ||
b3yond | df32f3c614 | ||
b3yond | d7dea7df00 | ||
b3yond | 96ef5e2a3f | ||
b3yond | e64e3702f6 | ||
594b3fb5de | |||
b3yond | d22c85da1b | ||
b3yond | 4aa4846527 | ||
b3yond | 7a1a857ab4 | ||
b3yond | cbf16b8f74 | ||
b3yond | fb24c758a8 | ||
b3yond | 15d2c75b5a | ||
b3yond | 2c21fb09ca | ||
b3yond | 42aa60a968 | ||
b3yond | a4eef4b086 | ||
b3yond | 9e38906898 | ||
b3yond | b1348e5578 | ||
b3yond | 1ee464cf97 | ||
b3yond | 3ee52532d2 | ||
b3yond | 357d6c4fc2 | ||
b3yond | 36f919826f | ||
b3yond | 694a930d73 | ||
b3yond | d6a94432c8 | ||
aefe78eb50 | |||
b3yond | 150e3579b7 | ||
f4b8300ac1 |
3
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
|||
*.swp
|
||||
*.pyc
|
||||
*.egg-info/
|
||||
*.eggs
|
||||
.idea/
|
||||
__pycache__/
|
||||
last_mention
|
||||
|
@ -17,4 +19,3 @@ include/
|
|||
lib/
|
||||
share/
|
||||
local/
|
||||
venv/
|
||||
|
|
144
README.md
|
@ -32,13 +32,15 @@ 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,
|
||||
In version 3, 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.
|
||||
bots for multiple citys, which run in parallel. This way, you do not have to
|
||||
host it yourself, if you lack the know-how. But it is easily possible to do so
|
||||
with docker: http://github.com/ticketfrei/docker-ticketfrei/
|
||||
|
||||
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!
|
||||
In https://github.com/ticketfrei/promotion, 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
|
||||
|
||||
|
@ -152,19 +154,10 @@ 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
|
||||
# create folder for socket & 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
|
||||
|
@ -187,17 +180,6 @@ 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:
|
||||
|
@ -267,113 +249,3 @@ sudo chown $USER:$USER -R /var/log/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
|
||||
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from bot import Bot
|
||||
import logging
|
||||
import mastodon
|
||||
import re
|
||||
from report import Report
|
||||
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
|
||||
|
||||
class MastodonBot(Bot):
|
||||
def crawl(self, user):
|
||||
"""
|
||||
Crawl mentions from Mastodon.
|
||||
|
||||
:return: list of statuses
|
||||
"""
|
||||
mentions = []
|
||||
try:
|
||||
m = mastodon.Mastodon(*user.get_masto_credentials())
|
||||
except TypeError:
|
||||
# No Mastodon Credentials in database.
|
||||
return mentions
|
||||
try:
|
||||
notifications = m.notifications()
|
||||
except mastodon.MastodonNetworkError:
|
||||
logger.error("Mastodon Network Error.")
|
||||
return mentions
|
||||
except mastodon.MastodonAPIError:
|
||||
try:
|
||||
logger.error("Mastodon API Error: " + m.instance()['urls']['streaming_api'] + ", city: " + str(user.uid))
|
||||
except mastodon.MastodonServerError:
|
||||
logger.error("Mastodon Server Error 500, can't get instance.")
|
||||
except mastodon.MastodonVersionError:
|
||||
logger.error("Mastodon Server Error 500, server version too low.")
|
||||
return mentions
|
||||
except mastodon.MastodonInternalServerError:
|
||||
try:
|
||||
logger.error("Mastodon Error: 500. Server: " + m.instance()['urls']['streaming_api'])
|
||||
except mastodon.MastodonServerError:
|
||||
logger.error("Mastodon Server Error 500, can't get instance.")
|
||||
except mastodon.MastodonVersionError:
|
||||
logger.error("Mastodon Server Error 500, server version too low.")
|
||||
return mentions
|
||||
except mastodon.MastodonBadGatewayError:
|
||||
try:
|
||||
logger.error("Mastodon Error: 502. Server: " + m.instance()['urls']['streaming_api'])
|
||||
except mastodon.MastodonServerError:
|
||||
logger.error("Mastodon Server Error 502, can't get instance.")
|
||||
except mastodon.MastodonVersionError:
|
||||
logger.error("Mastodon Server Error 502, server version too low.")
|
||||
return mentions
|
||||
except mastodon.MastodonServiceUnavailableError:
|
||||
try:
|
||||
logger.error("Mastodon Error: 503. Server: " + m.instance()['urls']['streaming_api'])
|
||||
except mastodon.MastodonServerError:
|
||||
logger.error("Mastodon Server Error 503, can't get instance.")
|
||||
except mastodon.MastodonVersionError:
|
||||
logger.error("Mastodon Server Error 503, server version too low.")
|
||||
return mentions
|
||||
except mastodon.MastodonGatewayTimeoutError:
|
||||
try:
|
||||
logger.error("Mastodon Error: 504. Server: " + m.instance()['urls']['streaming_api'])
|
||||
except mastodon.MastodonServerError:
|
||||
logger.error("Mastodon Server Error 504, can't get instance.")
|
||||
except mastodon.MastodonVersionError:
|
||||
logger.error("Mastodon Server Error 504, server version too low.")
|
||||
return mentions
|
||||
except mastodon.MastodonServerError:
|
||||
try:
|
||||
logger.error("Unknown Mastodon Server Error. Server: " + m.instance()['urls']['streaming_api'], exc_info=True)
|
||||
except mastodon.MastodonServerError:
|
||||
logger.error("Unknown Mastodon Server Error.", exc_info=True)
|
||||
except mastodon.MastodonVersionError:
|
||||
logger.error("Unknown Mastodon Server Error.", exc_info=True)
|
||||
return mentions
|
||||
for status in notifications:
|
||||
try:
|
||||
if (status['type'] == 'mention' and
|
||||
not user.toot_is_seen(status['status']['uri'])):
|
||||
# save state
|
||||
user.toot_witness(status['status']['uri'])
|
||||
# add mention to mentions
|
||||
text = re.sub(r'<[^>]*>', '', status['status']['content'])
|
||||
text = re.sub(
|
||||
"(?<=^|(?<=[^a-zA-Z0-9-_.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
|
||||
"", text)
|
||||
if status['status']['visibility'] == 'public':
|
||||
mentions.append(Report(status['account']['acct'],
|
||||
self,
|
||||
text,
|
||||
status['status']['id'],
|
||||
status['status']['created_at']))
|
||||
else:
|
||||
mentions.append(Report(status['account']['acct'],
|
||||
'mastodonPrivate',
|
||||
text,
|
||||
status['status']['id'],
|
||||
status['status']['created_at']))
|
||||
except TypeError:
|
||||
pass
|
||||
return mentions
|
||||
|
||||
def post(self, user, report):
|
||||
try:
|
||||
m = mastodon.Mastodon(*user.get_masto_credentials())
|
||||
except TypeError:
|
||||
return # no mastodon account for this user.
|
||||
if report.source == self:
|
||||
try:
|
||||
m.status_reblog(report.id)
|
||||
except Exception:
|
||||
logger.error('Error boosting: ' + report.id, exc_info=True)
|
||||
else:
|
||||
text = report.text
|
||||
if len(text) > 500:
|
||||
text = text[:500 - 4] + u' ...'
|
||||
try:
|
||||
m.toot(text)
|
||||
except Exception:
|
||||
logger.error('Error tooting: ' + user.get_city() + ': ' +
|
||||
report.id, exc_info=True)
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/ksh
|
||||
|
||||
daemon="/usr/local/bin/python3 /srv/ticketfrei/backend.py"
|
||||
daemon_user="root"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
rc_bg=YES
|
||||
rc_reload=NO
|
||||
|
||||
rc_start() {
|
||||
rc_exec "cd /srv/ticketfrei; /usr/bin/nice -n15 ${daemon} ${daemon_flags}"
|
||||
}
|
||||
|
||||
rc_cmd $1
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/ksh
|
||||
. /etc/borg-env
|
||||
export BORG_REPO=nathan@nephilim:repositories-borg/ticketfrei
|
||||
export BORG_RSH="ssh \
|
||||
-o TCPKeepAlive=no \
|
||||
-o ServerAliveInterval=15 \
|
||||
-o ServerAliveCountMax=10 \
|
||||
-o Compression=no"
|
||||
|
||||
rcctl stop backend_daemon
|
||||
rcctl stop frontend_daemon
|
||||
/usr/local/bin/borg create --stats ::'backup{now:%Y%m%d-%H%M}' /srv/ticketfrei /var/ticketfrei /etc
|
||||
rcctl start backend_daemon
|
||||
rcctl start frontend_daemon
|
||||
|
31
deployment/example.org.conf
Normal file
|
@ -0,0 +1,31 @@
|
|||
server {
|
||||
|
||||
listen 443 ssl;
|
||||
server_name example.org;
|
||||
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
access_log /var/log/nginx/example.org_access.log;
|
||||
error_log /var/log/nginx/example.org_error.log;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
|
||||
uwsgi_pass unix:///var/run/ticketfrei/ticketfrei.sock;
|
||||
}
|
||||
|
||||
location /.well-known/acme-challenge {
|
||||
root /var/www/acme;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.org;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/ksh
|
||||
|
||||
daemon="/usr/local/bin/python3 /srv/ticketfrei/frontend.py"
|
||||
daemon_user="frontend"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
rc_bg=YES
|
||||
rc_reload=NO
|
||||
|
||||
rc_start() {
|
||||
rc_exec "env > /tmp/envars; cd /srv/ticketfrei; ${daemon} ${daemon_flags}"
|
||||
}
|
||||
|
||||
rc_cmd $1
|
17
deployment/ticketfrei-backend.service
Normal file
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Ticketfrei Backend
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/srv/ticketfrei
|
||||
ExecStart=/srv/ticketfrei/bin/python3 backend.py
|
||||
# Requires systemd version 211 or newer
|
||||
#RuntimeDirectory=uwsgi
|
||||
Restart=always
|
||||
KillSignal=SIGQUIT
|
||||
Type=simple
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
17
deployment/ticketfrei-web.service
Normal file
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Ticketfrei Web Application
|
||||
After=syslog.target network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/srv/ticketfrei
|
||||
ExecStart=/usr/bin/uwsgi --ini /srv/ticketfrei/deployment/uwsgi.ini
|
||||
# Requires systemd version 211 or newer
|
||||
RuntimeDirectory=uwsgi
|
||||
Restart=always
|
||||
KillSignal=SIGQUIT
|
||||
Type=notify
|
||||
StandardError=syslog
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,9 +0,0 @@
|
|||
tweepy
|
||||
pytoml
|
||||
Mastodon.py
|
||||
bottle
|
||||
pyjwt
|
||||
pylibscrypt
|
||||
Markdown
|
||||
twx
|
||||
gitpython
|
37
setup.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from setuptools import setup
|
||||
import sys
|
||||
import os
|
||||
|
||||
PACKAGE_NAME = "ticketfrei"
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), PACKAGE_NAME))
|
||||
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version='',
|
||||
packages=[
|
||||
PACKAGE_NAME
|
||||
],
|
||||
url='https://github.com/ticketfrei/ticketfrei',
|
||||
license='ISC',
|
||||
author='',
|
||||
author_email='',
|
||||
description='',
|
||||
setup_requires=[
|
||||
'pytest-runner',
|
||||
],
|
||||
install_requires=[
|
||||
'bottle',
|
||||
'gitpython',
|
||||
'pyjwt',
|
||||
'Markdown',
|
||||
'Mastodon.py',
|
||||
'pylibscrypt',
|
||||
'pytoml',
|
||||
'tweepy',
|
||||
'twx',
|
||||
],
|
||||
tests_require=[
|
||||
'pytest',
|
||||
],
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
% rebase('template/wrapper.tpl', title='Login')
|
||||
% include('template/login-plain.tpl')
|
|
@ -1,164 +0,0 @@
|
|||
% rebase('template/wrapper.tpl')
|
||||
<a href="/logout/"><button>Logout</button></a>
|
||||
|
||||
% if enabled:
|
||||
<div id="enablebutton" style="float: right; padding: 2em;">Disable</div>
|
||||
% else:
|
||||
<div id="enablebutton" style="float: right; padding: 2em;" color="red">Enable</div>
|
||||
% end
|
||||
|
||||
<a class='button' style="padding: 1.5em;" href="/login/twitter">
|
||||
<picture>
|
||||
<source type='image/webp' sizes='20px' srcset="/static-cb/1517673283/twitter-20.webp 20w,/static-cb/1517673283/twitter-40.webp 40w,/static-cb/1517673283/twitter-80.webp 80w,"/>
|
||||
<source type='image/png' sizes='20px' srcset="/static-cb/1517673283/twitter-20.png 20w,/static-cb/1517673283/twitter-40.png 40w,/static-cb/1517673283/twitter-80.png 80w,"/>
|
||||
<img src="https://patriciaannbridewell.files.wordpress.com/2014/04/official-twitter-logo-tile.png" alt="" />
|
||||
</picture>
|
||||
Log in with Twitter
|
||||
</a>
|
||||
|
||||
<section>
|
||||
<h2>Log in with Mastodon</h2>
|
||||
<form action="/login/mastodon" method='post'>
|
||||
<label for="email">E-Mail of your Mastodon-Account</label>
|
||||
<input type="text" placeholder="Enter Email" name="email" id="email" required>
|
||||
|
||||
<label for="pass">Mastodon Password</label>
|
||||
<input type="password" placeholder="Enter Password" name="pass" id="pass" required>
|
||||
|
||||
<label>Mastodon instance:
|
||||
<input type='text' name='instance_url' list='instances' placeholder='social.example.net'/>
|
||||
</label>
|
||||
<datalist id='instances'>
|
||||
<option value=''>
|
||||
<option value='anticapitalist.party'>
|
||||
<option value='awoo.space'>
|
||||
<option value='cybre.space'>
|
||||
<option value='mastodon.social'>
|
||||
<option value='glitch.social'>
|
||||
<option value='botsin.space'>
|
||||
<option value='witches.town'>
|
||||
<option value='social.wxcafe.net'>
|
||||
<option value='monsterpit.net'>
|
||||
<option value='mastodon.xyz'>
|
||||
<option value='a.weirder.earth'>
|
||||
<option value='chitter.xyz'>
|
||||
<option value='sins.center'>
|
||||
<option value='dev.glitch.social'>
|
||||
<option value='computerfairi.es'>
|
||||
<option value='niu.moe'>
|
||||
<option value='icosahedron.website'>
|
||||
<option value='hostux.social'>
|
||||
<option value='hyenas.space'>
|
||||
<option value='instance.business'>
|
||||
<option value='mastodon.sdf.org'>
|
||||
<option value='pawoo.net'>
|
||||
<option value='pouet.it'>
|
||||
<option value='scalie.business'>
|
||||
<option value='sleeping.town'>
|
||||
<option value='social.koyu.space'>
|
||||
<option value='sunshinegardens.org'>
|
||||
<option value='vcity.network'>
|
||||
<option value='octodon.social'>
|
||||
<option value='soc.ialis.me'>
|
||||
</datalist>
|
||||
<input name='csrf' value='{{csrf}}' type='hidden' />
|
||||
<input name='confirm' value='Log in' type='submit'/>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<%
|
||||
# todo: hide this part, if there is already a telegram bot connected.
|
||||
%>
|
||||
<div>
|
||||
<h2>Connect with Telegram</h2>
|
||||
<p>
|
||||
If you have a Telegram account, you can register a bot there. Just
|
||||
write to @botfather. There are detailed instructions on
|
||||
<a href="https://botsfortelegram.com/project/the-bot-father/" target="_blank">
|
||||
Bots for Telegram</a>.
|
||||
</p>
|
||||
<p>
|
||||
The botfather will give you an API key - with the API key, Ticketfrei
|
||||
can use the Telegram bot. Enter it here:
|
||||
</p>
|
||||
<form action="/settings/telegram" method="post">
|
||||
<input type="text" name="apikey" placeholder="Telegram bot API key" id="apikey">
|
||||
<input name='csrf' value='{{csrf}}' type='hidden' />
|
||||
<input name='confirm' value='Login with Telegram' type='submit'/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Edit your city page</h2>
|
||||
<p>
|
||||
With your bot, we generated you a page, which you can use for promotion:
|
||||
<a href="/city/{{city}}" target="_blank">Ticketfrei {{city}}</a> You
|
||||
can change what your users will read there, and adjust it to your
|
||||
needs.
|
||||
</p>
|
||||
<p>
|
||||
<b>You should definitely adjust the Social Media, E-Mail, and Telegram
|
||||
profile links.</b>
|
||||
Also consider adding this link to the text: <a href="/city/mail/{{city}}"
|
||||
target="_blank">Link to the mail subscription page</a>. Your readers
|
||||
can use this to subscribe to mail notifications.
|
||||
</p>
|
||||
<p>
|
||||
So this is the default text we suggest:
|
||||
</p>
|
||||
<form action="/settings/markdown" method="post">
|
||||
<textarea id="markdown" rows="20" cols="70" name="markdown" wrap="physical">{{markdown}}</textarea>
|
||||
<input name='csrf' value='{{csrf}}' type='hidden' />
|
||||
<input name='confirm' value='Save' type='submit'/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Edit your mail subscription page</h2>
|
||||
<p>
|
||||
There is also a page where users can subscribe to mail notifications:
|
||||
<a href="/city/mail/{{city}}" target="_blank">Ticketfrei {{city}}</a>.
|
||||
You can change what your users will read there, and adjust it to your
|
||||
needs.
|
||||
</p>
|
||||
<p>
|
||||
So this is the default text we suggest:
|
||||
</p>
|
||||
<form action="/settings/mail_md" method="post">
|
||||
<textarea id="mail_md" rows="20" cols="70" name="mail_md" wrap="physical">{{mail_md}}</textarea>
|
||||
<input name='csrf' value='{{csrf}}' type='hidden' />
|
||||
<input name='confirm' value='Save' type='submit'/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Edit your trigger patterns</h2>
|
||||
<p>
|
||||
These words have to be contained in a report. If none of these
|
||||
expressions is in the report, it will be ignored by the bot. You can
|
||||
use the defaults, or enter some expressions specific to your city and
|
||||
language.
|
||||
</p>
|
||||
<form action="/settings/goodlist" method="post">
|
||||
<textarea id="goodlist" rows="8" cols="70" name="goodlist" wrap="physical">{{triggerwords}}</textarea>
|
||||
<input name='csrf' value='{{csrf}}' type='hidden' />
|
||||
<input name='confirm' value='Submit' type='submit'/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Edit the blocklist</h2>
|
||||
<p>
|
||||
These words are not allowed in reports. If you encounter spam, you can
|
||||
add more here - the bot will ignore reports which use such words.
|
||||
There are words which you can't exclude from the blocklist, e.g.
|
||||
certain racist, sexist, or antisemitic slurs.
|
||||
</p>
|
||||
<form action="/settings/blocklist" method="post">
|
||||
<textarea id="blocklist" rows="8" cols="70" name="blocklist" wrap="physical">{{badwords}}</textarea>
|
||||
<input name='csrf' value='{{csrf}}' type='hidden' />
|
||||
<input name='confirm' value='Submit' type='submit'/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
0
ticketfrei/__init__.py
Normal file
|
@ -10,7 +10,7 @@ from bot import Bot
|
|||
from config import config
|
||||
from db import db
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mailbot(Bot):
|
||||
|
@ -58,16 +58,30 @@ def make_report(msg, user):
|
|||
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:
|
||||
|
||||
if msg.is_multipart():
|
||||
text = []
|
||||
for part in msg.get_payload():
|
||||
if part.get_content_type() == "text":
|
||||
text.append(part.get_payload())
|
||||
elif part.get_content_type() == "application/pgp-signature":
|
||||
pass # ignore PGP signatures
|
||||
elif part.get_content_type() == "multipart/mixed":
|
||||
for p in part:
|
||||
if isinstance(p, str):
|
||||
text.append(p)
|
||||
elif p.get_content_type() == "text":
|
||||
text.append(part.get_payload())
|
||||
else:
|
||||
logger.error("unknown MIMEtype: " +
|
||||
p.get_content_type())
|
||||
else:
|
||||
logger.error("unknown MIMEtype: " +
|
||||
part.get_content_type())
|
||||
text = '\n'.join(text)
|
||||
else:
|
||||
text = msg.get_payload()
|
||||
post = report.Report(author, "mail", text, None, date)
|
||||
except UnboundLocalError:
|
||||
logger.error('No suitable message body')
|
||||
return
|
||||
user.save_seen_mail(date)
|
||||
return post
|
||||
|
73
ticketfrei/active_bots/mastodonbot.py
Executable file
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from bot import Bot
|
||||
import logging
|
||||
from mastodon import Mastodon
|
||||
import re
|
||||
from report import Report
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MastodonBot(Bot):
|
||||
def crawl(self, user):
|
||||
"""
|
||||
Crawl mentions from Mastodon.
|
||||
|
||||
:return: list of statuses
|
||||
"""
|
||||
mentions = []
|
||||
try:
|
||||
m = Mastodon(*user.get_masto_credentials())
|
||||
except TypeError:
|
||||
# logger.error("No Mastodon Credentials in database.", exc_info=True)
|
||||
return mentions
|
||||
try:
|
||||
notifications = m.notifications()
|
||||
except Exception:
|
||||
logger.error("Unknown Mastodon API Error.", exc_info=True)
|
||||
return mentions
|
||||
for status in notifications:
|
||||
if (status['type'] == 'mention' and
|
||||
not user.toot_is_seen(status['status']['uri'])):
|
||||
# save state
|
||||
user.toot_witness(status['status']['uri'])
|
||||
# add mention to mentions
|
||||
text = re.sub(r'<[^>]*>', '', status['status']['content'])
|
||||
text = re.sub(
|
||||
"(?<=^|(?<=[^a-zA-Z0-9-_.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
|
||||
"", text)
|
||||
if status['status']['visibility'] == 'public':
|
||||
mentions.append(Report(status['account']['acct'],
|
||||
self,
|
||||
text,
|
||||
status['status']['id'],
|
||||
status['status']['created_at']))
|
||||
else:
|
||||
mentions.append(Report(status['account']['acct'],
|
||||
'mastodonPrivate',
|
||||
text,
|
||||
status['status']['id'],
|
||||
status['status']['created_at']))
|
||||
return mentions
|
||||
|
||||
def post(self, user, report):
|
||||
try:
|
||||
m = Mastodon(*user.get_masto_credentials())
|
||||
except TypeError:
|
||||
return # no mastodon account for this user.
|
||||
if report.source == self:
|
||||
try:
|
||||
m.status_reblog(report.id)
|
||||
except Exception:
|
||||
logger.error('Error boosting: ' + report.id, exc_info=True)
|
||||
else:
|
||||
text = report.text
|
||||
if len(text) > 500:
|
||||
text = text[:500 - 4] + u' ...'
|
||||
try:
|
||||
m.toot(text)
|
||||
except Exception:
|
||||
logger.error('Error tooting: ' + user.get_city() + ': ' +
|
||||
report.id, exc_info=True)
|
|
@ -3,7 +3,8 @@ import logging
|
|||
from report import Report
|
||||
from twx.botapi import TelegramBot as Telegram
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelegramBot(Bot):
|
||||
|
@ -12,10 +13,9 @@ class TelegramBot(Bot):
|
|||
seen_tg = user.get_seen_tg()
|
||||
try:
|
||||
updates = tb.get_updates(offset=seen_tg + 1,
|
||||
allowed_updates="message",
|
||||
timeout=5).wait()
|
||||
allowed_updates="message").wait()
|
||||
except TypeError:
|
||||
updates = tb.get_updates(timeout=5).wait()
|
||||
updates = tb.get_updates().wait()
|
||||
reports = []
|
||||
if updates == None:
|
||||
return reports
|
||||
|
@ -23,7 +23,6 @@ class TelegramBot(Bot):
|
|||
# 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) +
|
||||
|
@ -32,25 +31,7 @@ class TelegramBot(Bot):
|
|||
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(
|
74
ticketfrei/active_bots/twitterDMs.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
import tweepy
|
||||
import re
|
||||
import requests
|
||||
import report
|
||||
from time import time
|
||||
from bot import Bot
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitterDMListener(Bot):
|
||||
|
||||
def get_api(self, user):
|
||||
keys = user.get_twitter_credentials()
|
||||
auth = tweepy.OAuthHandler(consumer_key=keys[0],
|
||||
consumer_secret=keys[1])
|
||||
auth.set_access_token(keys[2], # access_token_key
|
||||
keys[3]) # access_token_secret
|
||||
return tweepy.API(auth, wait_on_rate_limit=True)
|
||||
|
||||
def crawl(self, user):
|
||||
"""
|
||||
crawls all Tweets which mention the bot from the twitter rest API.
|
||||
|
||||
:return: reports: (list of report.Report objects)
|
||||
"""
|
||||
reports = []
|
||||
try:
|
||||
if user.get_last_twitter_request() + 60 > time():
|
||||
return reports
|
||||
except TypeError:
|
||||
user.set_last_twitter_request(time())
|
||||
try:
|
||||
api = self.get_api(user)
|
||||
except TypeError:
|
||||
return reports # no twitter account for this user.
|
||||
last_dm = user.get_seen_dm()
|
||||
try:
|
||||
if last_dm is None:
|
||||
mentions = api.direct_messages()
|
||||
else:
|
||||
mentions = api.direct_messages(since_id=last_dm[0])
|
||||
user.set_last_twitter_request(time())
|
||||
for status in mentions:
|
||||
text = re.sub(
|
||||
"(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-_]+)",
|
||||
"", status.text)
|
||||
reports.append(report.Report(status.author.screen_name,
|
||||
"twitterDM",
|
||||
text,
|
||||
status.id,
|
||||
status.created_at))
|
||||
user.save_seen_dm(last_dm)
|
||||
return reports
|
||||
except tweepy.RateLimitError:
|
||||
logger.error("Twitter API Error: Rate Limit Exceeded",
|
||||
exc_info=True)
|
||||
# :todo implement rate limiting
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("Twitter API Error: Bad Connection", exc_info=True)
|
||||
except tweepy.TweepError as terror:
|
||||
# Waiting for https://github.com/tweepy/tweepy/pull/1109 to get
|
||||
# merged, so direct messages work again
|
||||
if terror.api_code == 34:
|
||||
return reports
|
||||
logger.error("Twitter API Error: General Error", exc_info=True)
|
||||
return reports
|
||||
|
||||
def post(self, user, report):
|
||||
pass
|
|
@ -9,7 +9,7 @@ import report
|
|||
from bot import Bot
|
||||
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitterBot(Bot):
|
||||
|
@ -67,13 +67,13 @@ class TwitterBot(Bot):
|
|||
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)
|
||||
logger.error("Twitter API Error: General Error", exc_info=True)
|
||||
return []
|
||||
|
||||
def post(self, user, report):
|
||||
try:
|
||||
api = self.get_api(user)
|
||||
except TypeError:
|
||||
except IndexError:
|
||||
return # no twitter account for this user.
|
||||
try:
|
||||
if report.source == self:
|
|
@ -5,7 +5,7 @@ from config import config
|
|||
from db import db
|
||||
import logging
|
||||
from sendmail import sendmail
|
||||
from time import sleep
|
||||
|
||||
|
||||
def shutdown():
|
||||
try:
|
||||
|
@ -16,14 +16,11 @@ def shutdown():
|
|||
|
||||
|
||||
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 = logging.getLogger()
|
||||
fh = logging.FileHandler(config["log"]["log_backend"])
|
||||
fh.setLevel(logging.DEBUG)
|
||||
logger.addHandler(fh)
|
||||
|
||||
logger.info("Backend Daemon was started...")
|
||||
|
||||
bots = []
|
||||
for ActiveBot in active_bots.__dict__.values():
|
||||
|
@ -34,14 +31,12 @@ if __name__ == '__main__':
|
|||
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:
|
19
ticketfrei/bots/mail/settings.tpl
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
<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>
|
||||
|
51
ticketfrei/bots/mastodon/settings.tpl
Normal file
|
@ -0,0 +1,51 @@
|
|||
|
||||
<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>
|
||||
|
23
ticketfrei/bots/telegram/settings.tpl
Normal file
|
@ -0,0 +1,23 @@
|
|||
|
||||
<%
|
||||
# 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>
|
||||
|
10
ticketfrei/bots/twitter/settings.tpl
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
<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>
|
||||
|
0
ticketfrei/bots/twitterDMs/settings.tpl
Normal file
|
@ -1,6 +1,10 @@
|
|||
import pytoml as toml
|
||||
import os
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
TEMPLATE_DIR = os.path.join(ROOT_DIR, 'template', '')
|
||||
STATIC_DIR = os.path.join(ROOT_DIR, 'static', '')
|
||||
BOT_DIR = os.path.join(ROOT_DIR, 'bots')
|
||||
|
||||
def load_env():
|
||||
"""
|
||||
|
@ -9,7 +13,7 @@ def load_env():
|
|||
|
||||
:return: config dictionary of dictionaries.
|
||||
"""
|
||||
with open('config.toml.example') as defaultconf:
|
||||
with open(os.path.join(ROOT_DIR, 'config.toml.example')) as defaultconf:
|
||||
configdict = toml.load(defaultconf)
|
||||
|
||||
try:
|
||||
|
@ -54,12 +58,24 @@ def load_env():
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['LOG_FRONTEND'] != "":
|
||||
configdict['log']['log_frontend'] = os.environ['LOG_FRONTEND']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ['LOG_BACKEND'] != "":
|
||||
configdict['log']['log_backend'] = os.environ['LOG_BACKEND']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return configdict
|
||||
|
||||
|
||||
# read config in TOML format (https://github.com/toml-lang/toml#toml)
|
||||
try:
|
||||
with open('config.toml') as configfile:
|
||||
with open(os.path.join(ROOT_DIR, 'config.toml')) as configfile:
|
||||
config = toml.load(configfile)
|
||||
except FileNotFoundError:
|
||||
config = load_env()
|
|
@ -11,6 +11,11 @@ contact = "b3yond@riseup.net"
|
|||
|
||||
[mail]
|
||||
mbox_user = "root"
|
||||
aliases_path = "/etc/aliases"
|
||||
|
||||
[database]
|
||||
db_path = "/var/ticketfrei/db.sqlite"
|
||||
|
||||
[log]
|
||||
log_frontend = "/var/ticketfrei/frontend.log"
|
||||
log_backend = "/var/log/ticketfrei/backend.log"
|
|
@ -1,13 +1,12 @@
|
|||
from config import config
|
||||
import jwt
|
||||
import logging
|
||||
from os import urandom, system
|
||||
from os import urandom
|
||||
from pylibscrypt import scrypt_mcf
|
||||
import sqlite3
|
||||
from time import sleep, time
|
||||
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DB(object):
|
||||
|
@ -20,19 +19,6 @@ class DB(object):
|
|||
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):
|
||||
|
@ -257,12 +243,8 @@ u\d\d?"""
|
|||
(uid, "bastard"))
|
||||
else:
|
||||
uid = json['uid']
|
||||
with open("/etc/aliases", "a+") as f:
|
||||
with open(config['mail']['aliases_path'], "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,
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
import bottle
|
||||
from os import listdir, path
|
||||
from bottle import get, post, redirect, request, response, view
|
||||
from config import config
|
||||
from config import config, STATIC_DIR, TEMPLATE_DIR
|
||||
from db import db
|
||||
import logging
|
||||
import tweepy
|
||||
|
@ -18,13 +19,13 @@ def url(route):
|
|||
|
||||
|
||||
@get('/')
|
||||
@view('template/propaganda.tpl')
|
||||
@view('propaganda.tpl')
|
||||
def propaganda():
|
||||
pass
|
||||
|
||||
|
||||
@post('/register')
|
||||
@view('template/register.tpl')
|
||||
@view('register.tpl')
|
||||
def register_post():
|
||||
try:
|
||||
email = request.forms['email']
|
||||
|
@ -54,7 +55,7 @@ def register_post():
|
|||
|
||||
|
||||
@get('/confirm/<city>/<token>')
|
||||
@view('template/propaganda.tpl')
|
||||
@view('propaganda.tpl')
|
||||
def confirm(city, token):
|
||||
# check whether city already exists
|
||||
if db.by_city(city):
|
||||
|
@ -75,7 +76,7 @@ def version():
|
|||
|
||||
|
||||
@post('/login')
|
||||
@view('template/login.tpl')
|
||||
@view('login.tpl')
|
||||
def login_post():
|
||||
# check login
|
||||
try:
|
||||
|
@ -94,14 +95,14 @@ 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',
|
||||
return bottle.template('city.tpl', **citydict)
|
||||
return bottle.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')
|
||||
@view('mail.tpl')
|
||||
def display_mail_page(city):
|
||||
user = db.by_city(city)
|
||||
return user.state()
|
||||
|
@ -138,34 +139,34 @@ def unsubscribe(token):
|
|||
|
||||
|
||||
@get('/settings')
|
||||
@view('template/settings.tpl')
|
||||
@view('settings.tpl')
|
||||
def settings(user):
|
||||
return user.state()
|
||||
|
||||
|
||||
@post('/settings/markdown')
|
||||
@view('template/settings.tpl')
|
||||
@view('settings.tpl')
|
||||
def update_markdown(user):
|
||||
user.set_markdown(request.forms['markdown'])
|
||||
return user.state()
|
||||
|
||||
|
||||
@post('/settings/mail_md')
|
||||
@view('template/settings.tpl')
|
||||
@view('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')
|
||||
@view('settings.tpl')
|
||||
def update_trigger_patterns(user):
|
||||
user.set_trigger_words(request.forms['goodlist'])
|
||||
return user.state()
|
||||
|
||||
|
||||
@post('/settings/blocklist')
|
||||
@view('template/settings.tpl')
|
||||
@view('settings.tpl')
|
||||
def update_badwords(user):
|
||||
user.set_badwords(request.forms['blocklist'])
|
||||
return user.state()
|
||||
|
@ -186,12 +187,12 @@ def register_telegram(user):
|
|||
|
||||
@get('/static/<filename:path>')
|
||||
def static(filename):
|
||||
return bottle.static_file(filename, root='static')
|
||||
return bottle.static_file(filename, root=STATIC_DIR)
|
||||
|
||||
|
||||
@get('/guides/<filename:path>')
|
||||
def guides(filename):
|
||||
return bottle.static_file(filename, root='guides')
|
||||
# IS THIS USED?
|
||||
#@get('/guides/<filename:path>')
|
||||
#def guides(filename):
|
||||
# return bottle.static_file(filename, root='guides')
|
||||
|
||||
|
||||
@get('/logout/')
|
||||
|
@ -264,10 +265,12 @@ def login_mastodon(user):
|
|||
|
||||
|
||||
logger = logging.getLogger()
|
||||
fh = logging.FileHandler('/var/log/ticketfrei/error.log')
|
||||
fh = logging.FileHandler(config['log']['log_frontend'])
|
||||
fh.setLevel(logging.DEBUG)
|
||||
logger.addHandler(fh)
|
||||
|
||||
# TODO change TEMPLATE_PATH to BOTS_DIR after refactoring
|
||||
bottle.TEMPLATE_PATH.insert(0, TEMPLATE_DIR)
|
||||
application = bottle.default_app()
|
||||
bottle.install(SessionPlugin('/'))
|
||||
|
|
@ -8,7 +8,7 @@ import smtplib
|
|||
from socket import getfqdn
|
||||
|
||||
|
||||
logger = logging.getLogger("main")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sendmail(to, subject, city=None, body=''):
|
Before Width: | Height: | Size: 628 KiB After Width: | Height: | Size: 628 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
@ -1,4 +1,4 @@
|
|||
% rebase('template/wrapper.tpl')
|
||||
% rebase('wrapper.tpl')
|
||||
|
||||
<%
|
||||
import markdown as md
|
2
ticketfrei/template/login.tpl
Normal file
|
@ -0,0 +1,2 @@
|
|||
% rebase('wrapper.tpl', title='Login')
|
||||
% include('login-plain.tpl')
|
|
@ -1,4 +1,4 @@
|
|||
% rebase('template/wrapper.tpl')
|
||||
% rebase('wrapper.tpl')
|
||||
|
||||
<%
|
||||
import markdown as md
|
|
@ -1,4 +1,4 @@
|
|||
% rebase('template/wrapper.tpl')
|
||||
% rebase('wrapper.tpl')
|
||||
% if defined('info'):
|
||||
<div class="ui-widget">
|
||||
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
|
||||
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
<br>
|
||||
% end
|
||||
% include('template/login-plain.tpl')
|
||||
% include('login-plain.tpl')
|
||||
<h1>Features</h1>
|
||||
<p>
|
||||
Don't pay for public transport. Instead, warn each other
|
||||
|
@ -45,7 +45,7 @@
|
|||
share it with us, so others can use it, too!</li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
% include('template/register-plain.tpl')
|
||||
% include('register-plain.tpl')
|
||||
<h2>Our Mission</h2>
|
||||
<p>
|
||||
Public transportation is meant to provide an easy and
|
|
@ -1,4 +1,4 @@
|
|||
% rebase('template/wrapper.tpl', title='Register')
|
||||
% rebase('wrapper.tpl', title='Register')
|
||||
% if defined('info'):
|
||||
<div class="ui-widget">
|
||||
<div class="ui-state-highlight ui-corner-all" style="padding: 0.7em;">
|
||||
|
@ -6,5 +6,5 @@
|
|||
</div>
|
||||
</div>
|
||||
% else:
|
||||
% include('template/register-plain.tpl')
|
||||
% include('register-plain.tpl')
|
||||
% end
|
76
ticketfrei/template/settings.tpl
Normal file
|
@ -0,0 +1,76 @@
|
|||
% rebase('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
|
||||
|
||||
<%
|
||||
# import all the settings templates from bots/*/settings.tpl
|
||||
from config import BOT_DIR
|
||||
import os
|
||||
bots = os.listdir(BOT_DIR)
|
||||
|
||||
for bot in bots:
|
||||
include(os.path.join(BOT_DIR, bot, 'settings.tpl'), csrf=csrf, city=city)
|
||||
end
|
||||
%>
|
||||
|
||||
<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 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>
|
||||
|
||||
|
20
ticketfrei/tests/configs/webapptestconfig.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[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"
|
||||
|
||||
[log]
|
||||
log_frontend = "/var/ticketfrei/frontend.log"
|
||||
log_backend = "/var/log/ticketfrei/backend.log"
|
23
ticketfrei/tests/webapptests/test_login.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from webtest import TestApp
|
||||
import unittest
|
||||
import frontend
|
||||
|
||||
app = TestApp(frontend.application)
|
||||
|
||||
class TestLogin(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_login_not_registered(self):
|
||||
request = app.post('/login', {'email': '', 'pass': ''}, expect_errors=True)
|
||||
self.assertEqual(401, request.status_code)
|
||||
|
||||
def test_login_registered(self):
|
||||
request = app.post('/register', {'email': 'foo@abc.de', 'pass': 'bar', 'pass-repeat': 'bar', 'city': 'testcity'}, expect_errors=True)
|
||||
request = app.post('/login', {'email': 'foor@abc.de', 'pass': 'bar'}, expect_errors=False)
|
||||
self.assertEqual(200, request.status_code)
|
||||
|
0
ticketfrei/tests/webapptests/test_logout.py
Normal file
0
ticketfrei/tests/webapptests/test_mailhandling.py
Normal file
16
ticketfrei/tests/webapptests/test_register.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
from webtest import TestApp
|
||||
import unittest
|
||||
import frontend
|
||||
|
||||
app = TestApp(frontend.application)
|
||||
|
||||
class TestRegister(unittest.TestCase):
|
||||
|
||||
def test_register(self):
|
||||
request = app.post('/register', {'email': 'foo@abc.de', 'pass': 'bar', 'pass-repeat': 'bar', 'city': 'testcity'}, expect_errors=True)
|
||||
self.assertEqual(200, request.status_code)
|
||||
|
||||
def test_getRoot(self):
|
||||
request = app.get('/')
|
||||
self.assertEqual(200, request.status_code)
|
||||
|
0
ticketfrei/tests/webapptests/test_settings.py
Normal file
0
ticketfrei/tests/webapptests/test_statics.py
Normal file
|
@ -5,9 +5,7 @@ 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):
|
||||
|
@ -76,7 +74,6 @@ class User(object):
|
|||
break
|
||||
else:
|
||||
# no pattern matched
|
||||
logger.error("Message didn't trigger goodlist: " + report.text)
|
||||
return False
|
||||
default_badwords = """
|
||||
bitch
|
||||
|
@ -96,15 +93,12 @@ 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)
|
||||
for word in report.text.lower().splitlines():
|
||||
if word in badwords:
|
||||
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)
|
||||
for word in default_badwords.splitlines():
|
||||
if word in badwords:
|
||||
return False
|
||||
logger.info("Valid report: " + report.text + " | username: " + report.author)
|
||||
return True
|
||||
|
||||
def get_last_twitter_request(self):
|
||||
|
@ -287,8 +281,6 @@ schlitz
|
|||
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):
|