Compare commits
512 Commits
v1.20.0
...
imageproxy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dd685f319 | ||
|
|
63204575eb | ||
|
|
fb3b0be50e | ||
|
|
0c08738a66 | ||
|
|
21cf5b57c1 | ||
|
|
b5f47b1b73 | ||
|
|
05c140da78 | ||
|
|
8225e4098b | ||
|
|
90aeb6e1a7 | ||
|
|
12e364969b | ||
|
|
ca58c70206 | ||
|
|
551996ca14 | ||
|
|
bb79d564a8 | ||
|
|
878872799e | ||
|
|
aa7b8a0fc0 | ||
|
|
14810b2cc5 | ||
|
|
5017a9ba7e | ||
|
|
a040c7dd2e | ||
|
|
912ebbc409 | ||
|
|
e1fe63ab19 | ||
|
|
509f03ce65 | ||
|
|
64a897b52f | ||
|
|
2f66db5989 | ||
|
|
033f40c263 | ||
|
|
a78fe8ceb9 | ||
|
|
c6a973f7e1 | ||
|
|
d7647520c8 | ||
|
|
70f491fd27 | ||
|
|
f07f2803f8 | ||
|
|
4364ce5d6f | ||
|
|
7c3d738756 | ||
|
|
ede010c25d | ||
|
|
db09b8eb84 | ||
|
|
a0cd155730 | ||
|
|
b7814d9541 | ||
|
|
912b917a47 | ||
|
|
c0112828eb | ||
|
|
b3237b0c49 | ||
|
|
b22ef5ae83 | ||
|
|
8d6661511a | ||
|
|
607c534174 | ||
|
|
3b213889ca | ||
|
|
36dc51ef4a | ||
|
|
663cbd91f5 | ||
|
|
82fe0e7bbf | ||
|
|
324815d58d | ||
|
|
a67e3af172 | ||
|
|
0cd23f7883 | ||
|
|
1b296fcae5 | ||
|
|
84d7030f7d | ||
|
|
2fddc276de | ||
|
|
a92ef0a8a1 | ||
|
|
99eee9c758 | ||
|
|
56ff354021 | ||
|
|
ac4d5c8c88 | ||
|
|
c5fa76dab0 | ||
|
|
33bf3304a1 | ||
|
|
53d2d18b89 | ||
|
|
fa23a00014 | ||
|
|
81b24c6cb3 | ||
|
|
60a33a6492 | ||
|
|
9acb7698ef | ||
|
|
9e6ded6544 | ||
|
|
ff5f98558d | ||
|
|
a088b1b0b5 | ||
|
|
29adccb6d1 | ||
|
|
c6d39fcba3 | ||
|
|
fe2acddb5b | ||
|
|
3dde8c05ad | ||
|
|
f49da74c3a | ||
|
|
53babc1113 | ||
|
|
09f8302e74 | ||
|
|
665bfd64d2 | ||
|
|
cf5360f6f6 | ||
|
|
f1edfcebc0 | ||
|
|
ef9860b6cc | ||
|
|
4f920f011f | ||
|
|
b613ac4b89 | ||
|
|
e8dca43f44 | ||
|
|
46b60f9d24 | ||
|
|
a02942b7e0 | ||
|
|
693cf4250a | ||
|
|
ee9f6454e0 | ||
|
|
c5d99e00d8 | ||
|
|
7be0616d38 | ||
|
|
ea527f9598 | ||
|
|
8fcea988ca | ||
|
|
6b8b98c15b | ||
|
|
17798df342 | ||
|
|
2f2b8dc983 | ||
|
|
6e763b8453 | ||
|
|
09a9219fcd | ||
|
|
c8406b45d4 | ||
|
|
14a67b99ba | ||
|
|
7461acdd1f | ||
|
|
88a8e85b12 | ||
|
|
5a1656b8d0 | ||
|
|
8fad3a15cd | ||
|
|
ce4c2142e2 | ||
|
|
6f9c01c375 | ||
|
|
5e255a07f6 | ||
|
|
5314fda342 | ||
|
|
dfc6f6fd6e | ||
|
|
05a08b4c05 | ||
|
|
07d7282383 | ||
|
|
01bed3e307 | ||
|
|
aabf70d458 | ||
|
|
6d2558a921 | ||
|
|
28995ffdd6 | ||
|
|
e4d0b57f3c | ||
|
|
0054f362a7 | ||
|
|
12bcdf2d47 | ||
|
|
e709fc9ce3 | ||
|
|
e0b490fdc0 | ||
|
|
7964f5979a | ||
|
|
6ebab36877 | ||
|
|
afb699f8d3 | ||
|
|
d772f157eb | ||
|
|
1b81ff4d3b | ||
|
|
8c5d997c6e | ||
|
|
c065519cca | ||
|
|
df79159e2e | ||
|
|
1064885a2c | ||
|
|
60362abef1 | ||
|
|
d7d9131de8 | ||
|
|
c44cc8082c | ||
|
|
7a4335b8bc | ||
|
|
8e3930d092 | ||
|
|
5cbdaae5b3 | ||
|
|
c7aecd32be | ||
|
|
4820d11ce3 | ||
|
|
fc8cd3cfb8 | ||
|
|
fc9b8c2a5a | ||
|
|
9ec91fc52d | ||
|
|
2ae4a2ed5a | ||
|
|
dfa5d0c5a7 | ||
|
|
fc3a481e6f | ||
|
|
5ab8c2f0f1 | ||
|
|
5a0aa636f3 | ||
|
|
fb1d33d27a | ||
|
|
8d8577a941 | ||
|
|
70ac8fa6ab | ||
|
|
7088ffd321 | ||
|
|
e175e489e8 | ||
|
|
7efdf5cfef | ||
|
|
5fb01f01bf | ||
|
|
333dd60b32 | ||
|
|
4433c32afc | ||
|
|
d5190b0d76 | ||
|
|
58e1b924ca | ||
|
|
ac7714b997 | ||
|
|
778a602aa6 | ||
|
|
fd0b203f1e | ||
|
|
b28732ee74 | ||
|
|
d8f33a4111 | ||
|
|
396a620cf4 | ||
|
|
f7b3f4573d | ||
|
|
9ead6c1481 | ||
|
|
55dc6460d2 | ||
|
|
3aa12be544 | ||
|
|
35e1659b77 | ||
|
|
2a9e52d36b | ||
|
|
3f48905331 | ||
|
|
cf307e25d0 | ||
|
|
4046c00a01 | ||
|
|
4226e945e6 | ||
|
|
f93a2d8717 | ||
|
|
2910fcc1a4 | ||
|
|
8ff61b4517 | ||
|
|
4944463f56 | ||
|
|
b3bd7ac615 | ||
|
|
64a180ba8f | ||
|
|
5a2ce15f96 | ||
|
|
f6f4fe4fc6 | ||
|
|
a17260a4ee | ||
|
|
4019e82f4a | ||
|
|
79230c1b0e | ||
|
|
da3175e7bd | ||
|
|
d654113204 | ||
|
|
6e3d32a9d5 | ||
|
|
e1d6c4f5f5 | ||
|
|
085f0b49c6 | ||
|
|
5fe3b0b459 | ||
|
|
3efda30b98 | ||
|
|
683b855584 | ||
|
|
9c10e190bc | ||
|
|
19308b645b | ||
|
|
c46430c663 | ||
|
|
d976269f1a | ||
|
|
c8e93a9f52 | ||
|
|
d32bb30071 | ||
|
|
d5263acdf8 | ||
|
|
8872ad33ad | ||
|
|
7e29a621c3 | ||
|
|
dfb216a8df | ||
|
|
f75bdec756 | ||
|
|
0082870864 | ||
|
|
d0e1a95d9c | ||
|
|
f69fb47d69 | ||
|
|
4f52f82a15 | ||
|
|
7c07ac22ad | ||
|
|
afb87c525d | ||
|
|
9b0ce41fd7 | ||
|
|
5f7c0a3b24 | ||
|
|
f7d90f2f53 | ||
|
|
43aab057c8 | ||
|
|
bfcdd642fd | ||
|
|
a8537659e2 | ||
|
|
9620ceb842 | ||
|
|
5ef15e91d4 | ||
|
|
2358ed1b24 | ||
|
|
af8d8c330d | ||
|
|
14f643592c | ||
|
|
8c5cdb630e | ||
|
|
b18903b59b | ||
|
|
42f96618e2 | ||
|
|
0c0d3e1715 | ||
|
|
2b06bca015 | ||
|
|
78630b3071 | ||
|
|
15063d92cd | ||
|
|
15821fe796 | ||
|
|
7d558ad7a2 | ||
|
|
4242898e5d | ||
|
|
d24f024cca | ||
|
|
ff93be7a89 | ||
|
|
a47d222a47 | ||
|
|
9f62c251f2 | ||
|
|
aff20f1a6d | ||
|
|
6841a295ff | ||
|
|
7a584e1a6e | ||
|
|
00be054135 | ||
|
|
6eb4a803fd | ||
|
|
6503a82094 | ||
|
|
31f52580c2 | ||
|
|
2aa0c3cc84 | ||
|
|
a3b3525b78 | ||
|
|
d76f10c312 | ||
|
|
a1e0e37223 | ||
|
|
9a1c1c56e6 | ||
|
|
3a5fe25e12 | ||
|
|
f56b6021d8 | ||
|
|
380c88b5a3 | ||
|
|
dd1288dc3c | ||
|
|
258a23cd9a | ||
|
|
92fb54556a | ||
|
|
e81eb9c8d5 | ||
|
|
8ec4006cc7 | ||
|
|
b3fdb991d6 | ||
|
|
5b086bb559 | ||
|
|
934d581737 | ||
|
|
e85d1755f0 | ||
|
|
1c4fe1b80c | ||
|
|
f6ff5cba82 | ||
|
|
193e4e39b1 | ||
|
|
ab7d5a3feb | ||
|
|
2279208b00 | ||
|
|
a8e1863341 | ||
|
|
506974a50d | ||
|
|
996ba82663 | ||
|
|
68524adadf | ||
|
|
b8ee612b45 | ||
|
|
5db633d911 | ||
|
|
2f208d9239 | ||
|
|
0c81d0ae2b | ||
|
|
6167bdc7f0 | ||
|
|
b50a306e66 | ||
|
|
0b06cfffba | ||
|
|
85551539f0 | ||
|
|
3914659718 | ||
|
|
8fd229b739 | ||
|
|
d69da8e3ea | ||
|
|
9a64809542 | ||
|
|
ce034fddd4 | ||
|
|
e931d6a88b | ||
|
|
a8a3067ac9 | ||
|
|
64dab0c4b6 | ||
|
|
dd7146205a | ||
|
|
68a132f271 | ||
|
|
c7b1dcec4f | ||
|
|
7d0a93483a | ||
|
|
836caf0fe0 | ||
|
|
980e1c9eb1 | ||
|
|
e2a9ea91cf | ||
|
|
2a7318eca2 | ||
|
|
b067fd2e77 | ||
|
|
6a674c3c79 | ||
|
|
0ac2827468 | ||
|
|
054dfa4cbd | ||
|
|
74371d3fcb | ||
|
|
9d3ba8991d | ||
|
|
0e4722ea98 | ||
|
|
208a47b41d | ||
|
|
7fb2f3170c | ||
|
|
9663e87838 | ||
|
|
8dd1cd9045 | ||
|
|
643380038b | ||
|
|
27dfbabe2f | ||
|
|
15bbc35e65 | ||
|
|
c9e4638b34 | ||
|
|
ff2cd6dc2f | ||
|
|
aa6892da82 | ||
|
|
3fbc5f7751 | ||
|
|
a57e06d09b | ||
|
|
bbc89bb2c2 | ||
|
|
ab58570a0c | ||
|
|
cd520a0251 | ||
|
|
8bcf12e1a7 | ||
|
|
ec087e4687 | ||
|
|
ae4352068e | ||
|
|
2faa0e4219 | ||
|
|
2e3a9035c4 | ||
|
|
2e6f99d09e | ||
|
|
f437850a50 | ||
|
|
820c968f73 | ||
|
|
9d1f098d8a | ||
|
|
d7ecf8041a | ||
|
|
a123f42291 | ||
|
|
e6a7df6144 | ||
|
|
4bd9411d87 | ||
|
|
284683e7e5 | ||
|
|
868ced76a8 | ||
|
|
729241c0fe | ||
|
|
8d48723158 | ||
|
|
2fb2f1ae49 | ||
|
|
d5b8dd8909 | ||
|
|
dfbcbbbb47 | ||
|
|
08f6bd8bef | ||
|
|
31b8a7931b | ||
|
|
a4dd4f0429 | ||
|
|
bf927c50f0 | ||
|
|
5f7be4b433 | ||
|
|
9bf20df941 | ||
|
|
adc31962c0 | ||
|
|
0e9b8a1a82 | ||
|
|
6150447c85 | ||
|
|
dd86fb0e14 | ||
|
|
b483d5413f | ||
|
|
c80ff2e709 | ||
|
|
2181e57e42 | ||
|
|
c490df7f50 | ||
|
|
b9e1be57e4 | ||
|
|
c3d96c7459 | ||
|
|
b9386d5a47 | ||
|
|
1d8abd8f4b | ||
|
|
0bf61dda08 | ||
|
|
660b813ff7 | ||
|
|
ba3b108239 | ||
|
|
42b08f9bcd | ||
|
|
849cdd257d | ||
|
|
16b174d882 | ||
|
|
61e2c8a1c0 | ||
|
|
c7a7609763 | ||
|
|
13130a217c | ||
|
|
daa2d2989e | ||
|
|
ee6de95a52 | ||
|
|
1daf59b7db | ||
|
|
94e91e33b8 | ||
|
|
d91f537bdd | ||
|
|
436a4cce2b | ||
|
|
71f9fe469a | ||
|
|
76b0c8446c | ||
|
|
069c7de78c | ||
|
|
3eeaf3be22 | ||
|
|
1989eabf86 | ||
|
|
491f9ed679 | ||
|
|
000259fc88 | ||
|
|
078765fe44 | ||
|
|
45877bb3a4 | ||
|
|
eb3d067e26 | ||
|
|
db80c441ec | ||
|
|
849b814034 | ||
|
|
37a4dbe66b | ||
|
|
75ab56cad1 | ||
|
|
25b481ac0a | ||
|
|
893507691b | ||
|
|
ac7adde4b1 | ||
|
|
d0aecd0ee5 | ||
|
|
307b8f8dde | ||
|
|
9d033acfff | ||
|
|
2617c53abf | ||
|
|
bbf1143090 | ||
|
|
2a37608cb0 | ||
|
|
3dd5e7445e | ||
|
|
8dcbcd8b62 | ||
|
|
d00428eb7e | ||
|
|
0e2ea27f54 | ||
|
|
b2809ed12e | ||
|
|
a72b9a174a | ||
|
|
ecb3d83c57 | ||
|
|
2cfb59d042 | ||
|
|
4c3063cadf | ||
|
|
66885fedaa | ||
|
|
064eb9df04 | ||
|
|
c3cef1eed6 | ||
|
|
ba8c4d3d24 | ||
|
|
c99a2a554b | ||
|
|
749e7c6cd0 | ||
|
|
4d29087431 | ||
|
|
183b769ee2 | ||
|
|
14cf3912f0 | ||
|
|
720457e876 | ||
|
|
27d004d299 | ||
|
|
7f650a619e | ||
|
|
d7977dec84 | ||
|
|
99a8f3d5d6 | ||
|
|
c1b4256d44 | ||
|
|
ed78a2e06f | ||
|
|
55a90e0464 | ||
|
|
fb78136870 | ||
|
|
b477556698 | ||
|
|
fc5cca9def | ||
|
|
dc455bcd87 | ||
|
|
bda82d2792 | ||
|
|
a36e233051 | ||
|
|
8484c0f0aa | ||
|
|
ffb5b49521 | ||
|
|
c15dd183a0 | ||
|
|
0eca2d49ba | ||
|
|
57e7cbfd09 | ||
|
|
e94bd9b845 | ||
|
|
05bf8e3b3d | ||
|
|
3992b19be3 | ||
|
|
a678a61c23 | ||
|
|
b5ce0a786a | ||
|
|
d58579d308 | ||
|
|
0620c3e00f | ||
|
|
a8374ec779 | ||
|
|
24090235d1 | ||
|
|
bbaa687aa7 | ||
|
|
dadfe72b48 | ||
|
|
9cc3eba009 | ||
|
|
54a636d79e | ||
|
|
0087c1ef9d | ||
|
|
39881e0d04 | ||
|
|
39a09c5d92 | ||
|
|
663a9ba07b | ||
|
|
144ae69f5c | ||
|
|
3e07bed51b | ||
|
|
9de219fd80 | ||
|
|
4a25435f7a | ||
|
|
b0f32affcb | ||
|
|
99548ea65f | ||
|
|
325ee02b49 | ||
|
|
a60786d32c | ||
|
|
2976afd5d1 | ||
|
|
744c52ba18 | ||
|
|
c31c1fd92a | ||
|
|
36615ef656 | ||
|
|
53a5dffb26 | ||
|
|
74f3a77a84 | ||
|
|
a15f1ac223 | ||
|
|
19a626e237 | ||
|
|
43c2ee6b7b | ||
|
|
b1555bfcd5 | ||
|
|
d5541791b6 | ||
|
|
62b1cae0ab | ||
|
|
933a23c9c7 | ||
|
|
f2799349ab | ||
|
|
1d223cc16f | ||
|
|
b7101a403b | ||
|
|
493917d8b1 | ||
|
|
e12aec4ccd | ||
|
|
d4936e18ee | ||
|
|
beb9c0e959 | ||
|
|
14faae3fd1 | ||
|
|
6f1472addb | ||
|
|
2fa2c3afec | ||
|
|
6e938ba74c | ||
|
|
53a63367dc | ||
|
|
ddf5c7f665 | ||
|
|
4e331c7f14 | ||
|
|
5e60cb83de | ||
|
|
595d6ea3b6 | ||
|
|
71fa62fd6a | ||
|
|
be5bb11fe3 | ||
|
|
981ae74e5c | ||
|
|
2b66193969 | ||
|
|
ed304f7687 | ||
|
|
7ac7af4774 | ||
|
|
5fa0a7866a | ||
|
|
f24148d431 | ||
|
|
980023a80c | ||
|
|
b68a1d7ab9 | ||
|
|
2ef90902aa | ||
|
|
e115b0678c | ||
|
|
0bda16de6d | ||
|
|
fd6ba459f9 | ||
|
|
d503908a65 | ||
|
|
215839c423 | ||
|
|
783bc86aaf | ||
|
|
6e626c0f89 | ||
|
|
facdd35b11 | ||
|
|
ec8a88a7a8 | ||
|
|
1b1c94ffa0 | ||
|
|
bcd003685e | ||
|
|
59039a14a5 | ||
|
|
0d6e217405 | ||
|
|
64e1805b53 | ||
|
|
22d02edbd8 | ||
|
|
5a496f6858 | ||
|
|
f4209d7a67 | ||
|
|
077bdeb01c | ||
|
|
095494f96f | ||
|
|
6f230ee4b2 | ||
|
|
311e0218af | ||
|
|
3fee369dc1 | ||
|
|
e57f2dfe7d | ||
|
|
dd5de1787f | ||
|
|
62f1aecfaf | ||
|
|
4ce388c8aa | ||
|
|
cb5451fe5d | ||
|
|
5466a2d64d |
@@ -1,3 +1,5 @@
|
||||
.git
|
||||
data
|
||||
data*
|
||||
uploads
|
||||
*.pyc
|
||||
__pycache__
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
config.cfg
|
||||
*.env
|
||||
/config.cfg
|
||||
/*.env
|
||||
*.sqlite
|
||||
.vscode
|
||||
custom.css
|
||||
@@ -10,6 +10,7 @@ app/public/uploads
|
||||
app/public/thumbnails
|
||||
celerybeat-schedule
|
||||
/data
|
||||
.idea
|
||||
|
||||
# Created by https://www.gitignore.io/api/linux,macos,python,windows
|
||||
|
||||
|
||||
22
.gitlab-ci.yml
Normal file
22
.gitlab-ci.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
image: docker/compose
|
||||
services:
|
||||
- docker:dind
|
||||
cache:
|
||||
key: "$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- /var/lib/docker
|
||||
|
||||
# build:
|
||||
# stage: build
|
||||
# script:
|
||||
# - cp utils/gitlabci/* .
|
||||
# - docker-compose build
|
||||
|
||||
UI_Test:
|
||||
stage: test
|
||||
script:
|
||||
- cp utils/gitlabci/* .
|
||||
- docker-compose up -d
|
||||
- ./utils/run_migrations.sh
|
||||
- ./utils/tests_cov.sh
|
||||
- docker-compose down
|
||||
13
Dockerfile
13
Dockerfile
@@ -5,15 +5,18 @@ RUN groupadd -g 5123 cdb && \
|
||||
|
||||
WORKDIR /home/cdb
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip install -r ./requirements.txt
|
||||
RUN mkdir /var/cdb
|
||||
RUN chown -R cdb:cdb /var/cdb
|
||||
|
||||
COPY requirements.lock.txt requirements.lock.txt
|
||||
RUN pip install -r requirements.lock.txt
|
||||
RUN pip install gunicorn
|
||||
|
||||
COPY utils utils
|
||||
COPY config.cfg ./config.cfg
|
||||
COPY config.cfg config.cfg
|
||||
COPY migrations migrations
|
||||
COPY app app
|
||||
|
||||
RUN mkdir /home/cdb/app/public/uploads/
|
||||
RUN chown cdb:cdb /home/cdb -R
|
||||
RUN chown -R cdb:cdb /home/cdb
|
||||
|
||||
USER cdb
|
||||
|
||||
660
LICENSE.md
Normal file
660
LICENSE.md
Normal file
@@ -0,0 +1,660 @@
|
||||
### GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
### Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains
|
||||
free software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing
|
||||
under this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
### TERMS AND CONDITIONS
|
||||
|
||||
#### 0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public
|
||||
License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds
|
||||
of works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of
|
||||
an exact copy. The resulting work is called a "modified version" of
|
||||
the earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user
|
||||
through a computer network, with no transfer of a copy, is not
|
||||
conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices" to
|
||||
the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
#### 1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work for
|
||||
making modifications to it. "Object code" means any non-source form of
|
||||
a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users can
|
||||
regenerate automatically from other parts of the Corresponding Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that same
|
||||
work.
|
||||
|
||||
#### 2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not convey,
|
||||
without conditions so long as your license otherwise remains in force.
|
||||
You may convey covered works to others for the sole purpose of having
|
||||
them make modifications exclusively for you, or provide you with
|
||||
facilities for running those works, provided that you comply with the
|
||||
terms of this License in conveying all material for which you do not
|
||||
control copyright. Those thus making or running the covered works for
|
||||
you must do so exclusively on your behalf, under your direction and
|
||||
control, on terms that prohibit them from making any copies of your
|
||||
copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under the
|
||||
conditions stated below. Sublicensing is not allowed; section 10 makes
|
||||
it unnecessary.
|
||||
|
||||
#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such
|
||||
circumvention is effected by exercising rights under this License with
|
||||
respect to the covered work, and you disclaim any intention to limit
|
||||
operation or modification of the work as a means of enforcing, against
|
||||
the work's users, your or third parties' legal rights to forbid
|
||||
circumvention of technological measures.
|
||||
|
||||
#### 4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
#### 5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these
|
||||
conditions:
|
||||
|
||||
- a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
- b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under
|
||||
section 7. This requirement modifies the requirement in section 4
|
||||
to "keep intact all notices".
|
||||
- c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
- d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
#### 6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms of
|
||||
sections 4 and 5, provided that you also convey the machine-readable
|
||||
Corresponding Source under the terms of this License, in one of these
|
||||
ways:
|
||||
|
||||
- a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
- b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the Corresponding
|
||||
Source from a network server at no charge.
|
||||
- c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
- d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
- e) Convey the object code using peer-to-peer transmission,
|
||||
provided you inform other peers where the object code and
|
||||
Corresponding Source of the work are being offered to the general
|
||||
public at no charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal,
|
||||
family, or household purposes, or (2) anything designed or sold for
|
||||
incorporation into a dwelling. In determining whether a product is a
|
||||
consumer product, doubtful cases shall be resolved in favor of
|
||||
coverage. For a particular product received by a particular user,
|
||||
"normally used" refers to a typical or common use of that class of
|
||||
product, regardless of the status of the particular user or of the way
|
||||
in which the particular user actually uses, or expects or is expected
|
||||
to use, the product. A product is a consumer product regardless of
|
||||
whether the product has substantial commercial, industrial or
|
||||
non-consumer uses, unless such uses represent the only significant
|
||||
mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to
|
||||
install and execute modified versions of a covered work in that User
|
||||
Product from a modified version of its Corresponding Source. The
|
||||
information must suffice to ensure that the continued functioning of
|
||||
the modified object code is in no case prevented or interfered with
|
||||
solely because modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or
|
||||
updates for a work that has been modified or installed by the
|
||||
recipient, or for the User Product in which it has been modified or
|
||||
installed. Access to a network may be denied when the modification
|
||||
itself materially and adversely affects the operation of the network
|
||||
or violates the rules and protocols for communication across the
|
||||
network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
#### 7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders
|
||||
of that material) supplement the terms of this License with terms:
|
||||
|
||||
- a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
- b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
- c) Prohibiting misrepresentation of the origin of that material,
|
||||
or requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
- d) Limiting the use for publicity purposes of names of licensors
|
||||
or authors of the material; or
|
||||
- e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
- f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions
|
||||
of it) with contractual assumptions of liability to the recipient,
|
||||
for any liability that these contractual assumptions directly
|
||||
impose on those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions; the
|
||||
above requirements apply either way.
|
||||
|
||||
#### 8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your license
|
||||
from a particular copyright holder is reinstated (a) provisionally,
|
||||
unless and until the copyright holder explicitly and finally
|
||||
terminates your license, and (b) permanently, if the copyright holder
|
||||
fails to notify you of the violation by some reasonable means prior to
|
||||
60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
#### 9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or run
|
||||
a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
#### 10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
#### 11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims owned
|
||||
or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within the
|
||||
scope of its coverage, prohibits the exercise of, or is conditioned on
|
||||
the non-exercise of one or more of the rights that are specifically
|
||||
granted under this License. You may not convey a covered work if you
|
||||
are a party to an arrangement with a third party that is in the
|
||||
business of distributing software, under which you make payment to the
|
||||
third party based on the extent of your activity of conveying the
|
||||
work, and under which the third party grants, to any of the parties
|
||||
who would receive the covered work from you, a discriminatory patent
|
||||
license (a) in connection with copies of the covered work conveyed by
|
||||
you (or copies made from those copies), or (b) primarily for and in
|
||||
connection with specific products or compilations that contain the
|
||||
covered work, unless you entered into that arrangement, or that patent
|
||||
license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
#### 12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under
|
||||
this License and any other pertinent obligations, then as a
|
||||
consequence you may not convey it at all. For example, if you agree to
|
||||
terms that obligate you to collect a royalty for further conveying
|
||||
from those to whom you convey the Program, the only way you could
|
||||
satisfy both those terms and this License would be to refrain entirely
|
||||
from conveying the Program.
|
||||
|
||||
#### 13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your
|
||||
version supports such interaction) an opportunity to receive the
|
||||
Corresponding Source of your version by providing access to the
|
||||
Corresponding Source from a network server at no charge, through some
|
||||
standard or customary means of facilitating copying of software. This
|
||||
Corresponding Source shall include the Corresponding Source for any
|
||||
work covered by version 3 of the GNU General Public License that is
|
||||
incorporated pursuant to the following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
#### 14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Affero General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever
|
||||
published by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future versions
|
||||
of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
#### 15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
|
||||
WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
|
||||
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
||||
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
|
||||
CORRECTION.
|
||||
|
||||
#### 16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
|
||||
CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
|
||||
ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
|
||||
NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
|
||||
LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
|
||||
TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
|
||||
PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
#### 17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
### How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these
|
||||
terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to
|
||||
attach them to the start of each source file to most effectively state
|
||||
the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper
|
||||
mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for
|
||||
the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. For more information on this, and how to apply and follow
|
||||
the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
674
LICENSE.txt
674
LICENSE.txt
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
97
README.md
97
README.md
@@ -1,27 +1,92 @@
|
||||
# Content Database
|
||||
[](https://gitlab.com/minetest/contentdb/pipelines)
|
||||
|
||||
Content database for Minetest mods, games, and more.
|
||||
Content database for Minetest mods, games, and more.\
|
||||
Developed by rubenwardy, license AGPLv3.0+.
|
||||
|
||||
Developed by rubenwardy, license GPLv3.0+.
|
||||
See [Getting Started](docs/getting_started.md).
|
||||
|
||||
## How-tos
|
||||
|
||||
Note: you should first read one of the guides on the [Github repo wiki](https://github.com/minetest/contentdb/wiki)
|
||||
|
||||
```sh
|
||||
# Run celery worker
|
||||
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
|
||||
# Hot/live reload (only works with FLASK_DEBUG=1)
|
||||
./utils/reload.sh
|
||||
|
||||
# if sqlite
|
||||
python utils/setup.py -t
|
||||
rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db stamp head
|
||||
|
||||
# Create migration
|
||||
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate
|
||||
|
||||
# Run migration
|
||||
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade
|
||||
# Cold update a running version of CDB with minimal downtime (production)
|
||||
./utils/update.sh
|
||||
|
||||
# Enter docker
|
||||
docker exec -it contentdb_app_1 bash
|
||||
./utils/bash.sh
|
||||
|
||||
# Run migrations
|
||||
./utils/run_migrations.sh
|
||||
|
||||
# Create new migration
|
||||
./utils/create_migration.sh
|
||||
```
|
||||
|
||||
|
||||
### VSCode: Setting up Linting
|
||||
|
||||
* (optional) Install the [Docker extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker)
|
||||
* Install the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
|
||||
* Click no to installing pylint (we don't want it to be installed outside of a virtual env)
|
||||
* Set up a virtual env
|
||||
* Replace `psycopg2` with `psycopg2_binary` in requirements.txt (because postgresql won't be installed on the system)
|
||||
* `python3 -m venv env`
|
||||
* Click yes to prompt to select virtual env for workspace
|
||||
* Click yes to any prompts about installing pylint
|
||||
* `source env/bin/activate`
|
||||
* `pip install -r requirements`
|
||||
* `pip install pylint` (if a prompt didn't appear)
|
||||
* Undo changes to requirements.txt
|
||||
|
||||
### VSCode: Material Icon Folder Designations
|
||||
|
||||
```json
|
||||
"material-icon-theme.folders.associations": {
|
||||
"packages": "",
|
||||
"tasks": "",
|
||||
"api": "",
|
||||
"meta": "",
|
||||
"blueprints": "routes",
|
||||
"scss": "sass",
|
||||
"flatpages": "markdown",
|
||||
"data": "temp",
|
||||
"migrations": "archive",
|
||||
"textures": "images",
|
||||
"sounds": "audio"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Database
|
||||
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
|
||||
User "1" --> "*" Package
|
||||
User --> UserEmailVerification
|
||||
User "1" --> "*" Notification
|
||||
Package "1" --> "*" Release
|
||||
Package "1" --> "*" Dependency
|
||||
Package "1" --> "*" Tag
|
||||
Package "1" --> "*" MetaPackage : provides
|
||||
Release --> MinetestVersion
|
||||
Package --> License
|
||||
Dependency --> Package
|
||||
Dependency --> MetaPackage
|
||||
MetaPackage "1" --> "*" Package
|
||||
Package "1" --> "*" Screenshot
|
||||
Package "1" --> "*" Thread
|
||||
Thread "1" --> "*" Reply
|
||||
Thread "1" --> "*" User : watchers
|
||||
User "1" --> "*" Thread
|
||||
User "1" --> "*" Reply
|
||||
User "1" --> "*" ForumTopic
|
||||
|
||||
User --> "0..1" EmailPreferences
|
||||
User "1" --> "*" APIToken
|
||||
APIToken --> Package
|
||||
```
|
||||
|
||||
@@ -1,93 +1,116 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from flask_gravatar import Gravatar
|
||||
import flask_menu as menu
|
||||
from flask_mail import Mail
|
||||
from flaskext.markdown import Markdown
|
||||
from flask_github import GitHub
|
||||
from flask_wtf.csrf import CsrfProtect
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from flask_flatpages import FlatPages
|
||||
from flask_babel import Babel
|
||||
from flask_login import logout_user, current_user, LoginManager
|
||||
import os, redis
|
||||
|
||||
app = Flask(__name__, static_folder="public/static")
|
||||
app.config["FLATPAGES_ROOT"] = "flatpages"
|
||||
app.config["FLATPAGES_EXTENSION"] = ".md"
|
||||
app.config["FLATPAGES_MARKDOWN_EXTENSIONS"] = ["fenced_code", "tables", "codehilite", 'toc']
|
||||
app.config["FLATPAGES_EXTENSION_CONFIG"] = {
|
||||
"fenced_code": {},
|
||||
"tables": {},
|
||||
"codehilite": {
|
||||
"linenums": "True"
|
||||
}
|
||||
}
|
||||
app.config.from_pyfile(os.environ["FLASK_CONFIG"])
|
||||
|
||||
r = redis.Redis.from_url(app.config["REDIS_URL"])
|
||||
|
||||
menu.Menu(app=app)
|
||||
markdown = Markdown(app, extensions=["fenced_code"], safe_mode=True, output_format="html5")
|
||||
github = GitHub(app)
|
||||
csrf = CsrfProtect(app)
|
||||
csrf = CSRFProtect(app)
|
||||
mail = Mail(app)
|
||||
pages = FlatPages(app)
|
||||
babel = Babel(app)
|
||||
gravatar = Gravatar(app,
|
||||
size=58,
|
||||
rating='g',
|
||||
default='mp',
|
||||
rating="g",
|
||||
default="mp",
|
||||
force_default=False,
|
||||
force_lower=False,
|
||||
use_ssl=True,
|
||||
base_url=None)
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "users.login"
|
||||
|
||||
from .sass import sass
|
||||
sass(app)
|
||||
|
||||
|
||||
if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]:
|
||||
from .maillogger import register_mail_error_handler
|
||||
register_mail_error_handler(app, mail)
|
||||
from .maillogger import build_handler
|
||||
app.logger.addHandler(build_handler(app))
|
||||
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
return request.accept_languages.best_match(app.config['LANGUAGES'].keys())
|
||||
from app.utils.markdown import init_app
|
||||
init_app(app)
|
||||
|
||||
# @babel.localeselector
|
||||
# def get_locale():
|
||||
# return request.accept_languages.best_match(app.config["LANGUAGES"].keys())
|
||||
|
||||
from . import models, template_filters
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return models.User.query.filter_by(username=user_id).first()
|
||||
|
||||
from . import models, tasks, template_filters
|
||||
|
||||
from .blueprints import create_blueprints
|
||||
create_blueprints(app)
|
||||
|
||||
from flask_login import logout_user
|
||||
|
||||
@app.route("/uploads/<path:path>")
|
||||
def send_upload(path):
|
||||
return send_from_directory("public/uploads", path)
|
||||
return send_from_directory(app.config["UPLOAD_DIR"], path)
|
||||
|
||||
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
||||
@app.route('/<path:path>/')
|
||||
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { "path": "help" })
|
||||
@app.route("/<path:path>/")
|
||||
def flatpage(path):
|
||||
page = pages.get_or_404(path)
|
||||
template = page.meta.get('template', 'flatpage.html')
|
||||
return render_template(template, page=page)
|
||||
page = pages.get_or_404(path)
|
||||
template = page.meta.get("template", "flatpage.html")
|
||||
return render_template(template, page=page)
|
||||
|
||||
@app.before_request
|
||||
def check_for_ban():
|
||||
if current_user.is_authenticated:
|
||||
if current_user.rank == models.UserRank.BANNED:
|
||||
flash("You have been banned.", "error")
|
||||
flash("You have been banned.", "danger")
|
||||
logout_user()
|
||||
return redirect(url_for('user.login'))
|
||||
return redirect(url_for("users.login"))
|
||||
elif current_user.rank == models.UserRank.NOT_JOINED:
|
||||
current_user.rank = models.UserRank.MEMBER
|
||||
models.db.session.commit()
|
||||
|
||||
from .utils import clearNotifications
|
||||
|
||||
@app.before_request
|
||||
def check_for_notifications():
|
||||
if current_user.is_authenticated:
|
||||
clearNotifications(request.path)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ from flask import Blueprint
|
||||
|
||||
bp = Blueprint("admin", __name__)
|
||||
|
||||
from . import admin, licenseseditor, tagseditor, versioneditor
|
||||
from . import admin, audit, licenseseditor, tagseditor, versioneditor, warningseditor, email
|
||||
|
||||
@@ -1,51 +1,90 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from celery import group
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
import flask_menu as menu
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from celery import uuid
|
||||
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies, makeVCSRelease
|
||||
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
||||
from flask_login import current_user, login_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from app.utils import loginUser, rank_required, triggerNotif
|
||||
import datetime
|
||||
from wtforms.validators import InputRequired, Length
|
||||
|
||||
from app.models import *
|
||||
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
|
||||
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
|
||||
from app.utils import rank_required, addAuditLog, addNotification
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/admin/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def admin_page():
|
||||
if request.method == "POST":
|
||||
action = request.form["action"]
|
||||
|
||||
if action == "delstuckreleases":
|
||||
PackageRelease.query.filter(PackageRelease.task_id != None).delete()
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
elif action == "checkreleases":
|
||||
releases = PackageRelease.query.filter(PackageRelease.url.like("/uploads/%")).all()
|
||||
|
||||
tasks = []
|
||||
for release in releases:
|
||||
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||
tasks.append(checkZipRelease.s(release.id, zippath))
|
||||
|
||||
result = group(tasks).apply_async()
|
||||
|
||||
while not result.ready():
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
return redirect(url_for("todo.view_editor"))
|
||||
|
||||
elif action == "reimportpackages":
|
||||
tasks = []
|
||||
for package in Package.query.filter(Package.state!=PackageState.DELETED).all():
|
||||
release = package.releases.first()
|
||||
if release:
|
||||
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||
tasks.append(checkZipRelease.s(release.id, zippath))
|
||||
|
||||
result = group(tasks).apply_async()
|
||||
|
||||
while not result.ready():
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
return redirect(url_for("todo.view_editor"))
|
||||
|
||||
elif action == "importmodlist":
|
||||
task = importTopicList.delay()
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("todo.topics")))
|
||||
|
||||
elif action == "checkusers":
|
||||
task = checkAllForumAccounts.delay()
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
|
||||
|
||||
elif action == "importscreenshots":
|
||||
packages = Package.query \
|
||||
.filter_by(soft_deleted=False) \
|
||||
.filter(Package.state!=PackageState.DELETED) \
|
||||
.outerjoin(PackageScreenshot, Package.id==PackageScreenshot.package_id) \
|
||||
.filter(PackageScreenshot.id==None) \
|
||||
.all()
|
||||
@@ -53,56 +92,102 @@ def admin_page():
|
||||
importRepoScreenshot.delay(package.id)
|
||||
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
elif action == "restore":
|
||||
package = Package.query.get(request.form["package"])
|
||||
if package is None:
|
||||
flash("Unknown package", "error")
|
||||
flash("Unknown package", "danger")
|
||||
else:
|
||||
package.soft_deleted = False
|
||||
package.state = PackageState.READY_FOR_REVIEW
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "importdepends":
|
||||
task = importAllDependencies.delay()
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("admin.admin_page")))
|
||||
elif action == "modprovides":
|
||||
packages = Package.query.filter_by(type=PackageType.MOD).all()
|
||||
mpackage_cache = {}
|
||||
for p in packages:
|
||||
if len(p.provides) == 0:
|
||||
p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache))
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "recalcscores":
|
||||
for p in Package.query.all():
|
||||
p.setStartScore()
|
||||
p.recalcScore()
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
elif action == "vcsrelease":
|
||||
for package in Package.query.filter(Package.repo.isnot(None)).all():
|
||||
if package.releases.count() != 0:
|
||||
continue
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = datetime.date.today().isoformat()
|
||||
rel.url = ""
|
||||
rel.task_id = uuid()
|
||||
rel.approved = True
|
||||
db.session.add(rel)
|
||||
db.session.commit()
|
||||
elif action == "cleanuploads":
|
||||
upload_dir = app.config['UPLOAD_DIR']
|
||||
|
||||
makeVCSRelease.apply_async((rel.id, "master"), task_id=rel.task_id)
|
||||
(_, _, filenames) = next(os.walk(upload_dir))
|
||||
existing_uploads = set(filenames)
|
||||
|
||||
msg = "{}: Release {} created".format(package.title, rel.title)
|
||||
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||
db.session.commit()
|
||||
if len(existing_uploads) != 0:
|
||||
def getURLsFromDB(column):
|
||||
results = db.session.query(column).filter(column != None, column != "").all()
|
||||
return set([os.path.basename(x[0]) for x in results])
|
||||
|
||||
release_urls = getURLsFromDB(PackageRelease.url)
|
||||
screenshot_urls = getURLsFromDB(PackageScreenshot.url)
|
||||
|
||||
db_urls = release_urls.union(screenshot_urls)
|
||||
unreachable = existing_uploads.difference(db_urls)
|
||||
|
||||
import sys
|
||||
print("On Disk: ", existing_uploads, file=sys.stderr)
|
||||
print("In DB: ", db_urls, file=sys.stderr)
|
||||
print("Unreachable: ", unreachable, file=sys.stderr)
|
||||
|
||||
for filename in unreachable:
|
||||
os.remove(os.path.join(upload_dir, filename))
|
||||
|
||||
flash("Deleted " + str(len(unreachable)) + " unreachable uploads", "success")
|
||||
else:
|
||||
flash("No downloads to create", "danger")
|
||||
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
elif action == "delmetapackages":
|
||||
query = MetaPackage.query.filter(~MetaPackage.dependencies.any(), ~MetaPackage.packages.any())
|
||||
count = query.count()
|
||||
query.delete(synchronize_session=False)
|
||||
db.session.commit()
|
||||
|
||||
flash("Deleted " + str(count) + " unused meta packages", "success")
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
elif action == "delremovedpackages":
|
||||
query = Package.query.filter_by(state=PackageState.DELETED)
|
||||
count = query.count()
|
||||
for pkg in query.all():
|
||||
pkg.review_thread = None
|
||||
db.session.delete(pkg)
|
||||
db.session.commit()
|
||||
|
||||
flash("Deleted {} soft deleted packages packages".format(count), "success")
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
elif action == "addupdateconfig":
|
||||
added = 0
|
||||
for pkg in Package.query.filter(Package.repo != None, Package.releases.any(), Package.update_config == None).all():
|
||||
pkg.update_config = PackageUpdateConfig()
|
||||
pkg.update_config.auto_created = True
|
||||
|
||||
release: PackageRelease = pkg.releases.first()
|
||||
if release and release.commit_hash:
|
||||
pkg.update_config.last_commit = release.commit_hash
|
||||
|
||||
db.session.add(pkg.update_config)
|
||||
added += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash("Added {} update configs".format(added), "success")
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
elif action == "runupdateconfig":
|
||||
check_for_updates.delay()
|
||||
|
||||
flash("Started update configs", "success")
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
else:
|
||||
flash("Unknown action: " + action, "error")
|
||||
flash("Unknown action: " + action, "danger")
|
||||
|
||||
deleted_packages = Package.query.filter_by(soft_deleted=True).all()
|
||||
deleted_packages = Package.query.filter(Package.state==PackageState.DELETED).all()
|
||||
return render_template("admin/list.html", deleted_packages=deleted_packages)
|
||||
|
||||
class SwitchUserForm(FlaskForm):
|
||||
@@ -114,15 +199,38 @@ class SwitchUserForm(FlaskForm):
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def switch_user():
|
||||
form = SwitchUserForm(formdata=request.form)
|
||||
if request.method == "POST" and form.validate():
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form["username"].data).first()
|
||||
if user is None:
|
||||
flash("Unable to find user", "error")
|
||||
elif loginUser(user):
|
||||
flash("Unable to find user", "danger")
|
||||
elif login_user(user):
|
||||
return redirect(url_for("users.profile", username=current_user.username))
|
||||
else:
|
||||
flash("Unable to login as user", "error")
|
||||
flash("Unable to login as user", "danger")
|
||||
|
||||
|
||||
# Process GET or invalid POST
|
||||
return render_template("admin/switch_user.html", form=form)
|
||||
|
||||
|
||||
class SendNotificationForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(1, 300)])
|
||||
url = StringField("URL", [InputRequired(), Length(1, 100)], default="/")
|
||||
submit = SubmitField("Send")
|
||||
|
||||
|
||||
@bp.route("/admin/send-notification/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def send_bulk_notification():
|
||||
form = SendNotificationForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user,
|
||||
"Sent bulk notification", None, None, form.title.data)
|
||||
|
||||
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).all()
|
||||
addNotification(users, current_user, NotificationType.OTHER, form.title.data, form.url.data, None)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
return render_template("admin/send_bulk_notification.html", form=form)
|
||||
|
||||
46
app/blueprints/admin/audit.py
Normal file
46
app/blueprints/admin/audit.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import render_template, request, abort
|
||||
from app.models import db, AuditLogEntry, UserRank, User
|
||||
from app.utils import rank_required, get_int_or_abort
|
||||
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/admin/audit/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def audit():
|
||||
page = get_int_or_abort(request.args.get("page"), 1)
|
||||
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||
|
||||
query = AuditLogEntry.query.order_by(db.desc(AuditLogEntry.created_at))
|
||||
|
||||
if "username" in request.args:
|
||||
user = User.query.filter_by(username=request.args.get("username")).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
query = query.filter_by(causer=user)
|
||||
|
||||
pagination = query.paginate(page, num, True)
|
||||
return render_template("admin/audit.html", log=pagination.items, pagination=pagination)
|
||||
|
||||
|
||||
@bp.route("/admin/audit/<int:id>/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def audit_view(id):
|
||||
entry = AuditLogEntry.query.get(id)
|
||||
return render_template("admin/audit_view.html", entry=entry)
|
||||
79
app/blueprints/admin/email.py
Normal file
79
app/blueprints/admin/email.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_login import current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.utils.markdown import render_markdown
|
||||
from app.models import *
|
||||
from app.tasks.emails import send_user_email
|
||||
from app.utils import rank_required, addAuditLog
|
||||
from . import bp
|
||||
|
||||
|
||||
class SendEmailForm(FlaskForm):
|
||||
subject = StringField("Subject", [InputRequired(), Length(1, 300)])
|
||||
text = TextAreaField("Message", [InputRequired()])
|
||||
submit = SubmitField("Send")
|
||||
|
||||
|
||||
@bp.route("/admin/send-email/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def send_single_email():
|
||||
username = request.args["username"]
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
next_url = url_for("users.profile", username=user.username)
|
||||
|
||||
if user.email is None:
|
||||
flash("User has no email address!", "danger")
|
||||
return redirect(next_url)
|
||||
|
||||
form = SendEmailForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user,
|
||||
"Sent email to {}".format(user.display_name), url_for("users.profile", username=username))
|
||||
|
||||
text = form.text.data
|
||||
html = render_markdown(text)
|
||||
task = send_user_email.delay(user.email, form.subject.data, text, html)
|
||||
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
||||
|
||||
return render_template("admin/send_email.html", form=form, user=user)
|
||||
|
||||
|
||||
@bp.route("/admin/send-bulk-email/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def send_bulk_email():
|
||||
form = SendEmailForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user,
|
||||
"Sent bulk email", None, None, form.text.data)
|
||||
|
||||
text = form.text.data
|
||||
html = render_markdown(text)
|
||||
for user in User.query.filter(User.email != None).all():
|
||||
send_user_email.delay(user.email, form.subject.data, text, html)
|
||||
|
||||
return redirect(url_for("admin.admin_page"))
|
||||
|
||||
return render_template("admin/send_bulk_email.html", form=form)
|
||||
@@ -1,28 +1,29 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import *
|
||||
from app.utils import rank_required
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/licenses/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
@@ -47,7 +48,7 @@ def create_edit_license(name=None):
|
||||
form = LicenseForm(formdata=request.form, obj=license)
|
||||
if request.method == "GET" and license is None:
|
||||
form.is_foss.data = True
|
||||
elif request.method == "POST" and form.validate():
|
||||
elif form.validate_on_submit():
|
||||
if license is None:
|
||||
license = License(form.name.data)
|
||||
db.session.add(license)
|
||||
|
||||
@@ -1,42 +1,54 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.utils import rank_required
|
||||
|
||||
from app.models import *
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/tags/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
@login_required
|
||||
def tag_list():
|
||||
return render_template("admin/tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
|
||||
if not Permission.EDIT_TAGS.check(current_user):
|
||||
abort(403)
|
||||
|
||||
query = Tag.query
|
||||
|
||||
if request.args.get("sort") == "views":
|
||||
query = query.order_by(db.desc(Tag.views))
|
||||
else:
|
||||
query = query.order_by(db.asc(Tag.title))
|
||||
|
||||
return render_template("admin/tags/list.html", tags=query.all())
|
||||
|
||||
class TagForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||
submit = SubmitField("Save")
|
||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||
description = TextAreaField("Description", [Optional(), Length(0, 500)])
|
||||
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@bp.route("/tags/new/", methods=["GET", "POST"])
|
||||
@bp.route("/tags/<name>/edit/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
@login_required
|
||||
def create_edit_tag(name=None):
|
||||
tag = None
|
||||
if name is not None:
|
||||
@@ -44,14 +56,22 @@ def create_edit_tag(name=None):
|
||||
if tag is None:
|
||||
abort(404)
|
||||
|
||||
if not Permission.checkPerm(current_user, Permission.EDIT_TAGS if tag else Permission.CREATE_TAG):
|
||||
abort(403)
|
||||
|
||||
form = TagForm(formdata=request.form, obj=tag)
|
||||
if request.method == "POST" and form.validate():
|
||||
if form.validate_on_submit():
|
||||
if tag is None:
|
||||
tag = Tag(form.title.data)
|
||||
tag.description = form.description.data
|
||||
db.session.add(tag)
|
||||
else:
|
||||
form.populate_obj(tag)
|
||||
db.session.commit()
|
||||
return redirect(url_for("admin.create_edit_tag", name=tag.name))
|
||||
|
||||
if Permission.EDIT_TAGS.check(current_user):
|
||||
return redirect(url_for("admin.create_edit_tag", name=tag.name))
|
||||
else:
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
return render_template("admin/tags/edit.html", tag=tag, form=form)
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import *
|
||||
from app.utils import rank_required
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/versions/")
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
@@ -45,7 +46,7 @@ def create_edit_version(name=None):
|
||||
abort(404)
|
||||
|
||||
form = VersionForm(formdata=request.form, obj=version)
|
||||
if request.method == "POST" and form.validate():
|
||||
if form.validate_on_submit():
|
||||
if version is None:
|
||||
version = MinetestRelease(form.name.data)
|
||||
db.session.add(version)
|
||||
|
||||
60
app/blueprints/admin/warningseditor.py
Normal file
60
app/blueprints/admin/warningseditor.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import *
|
||||
from app.utils import rank_required
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/admin/warnings/")
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def warning_list():
|
||||
return render_template("admin/warnings/list.html", warnings=ContentWarning.query.order_by(db.asc(ContentWarning.title)).all())
|
||||
|
||||
class WarningForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||
description = TextAreaField("Description", [Optional(), Length(0, 500)])
|
||||
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@bp.route("/admin/warnings/new/", methods=["GET", "POST"])
|
||||
@bp.route("/admin/warnings/<name>/edit/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def create_edit_warning(name=None):
|
||||
warning = None
|
||||
if name is not None:
|
||||
warning = ContentWarning.query.filter_by(name=name).first()
|
||||
if warning is None:
|
||||
abort(404)
|
||||
|
||||
form = WarningForm(formdata=request.form, obj=warning)
|
||||
if form.validate_on_submit():
|
||||
if warning is None:
|
||||
warning = ContentWarning(form.title.data, form.description.data)
|
||||
db.session.add(warning)
|
||||
else:
|
||||
form.populate_obj(warning)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("admin.warning_list"))
|
||||
|
||||
return render_template("admin/warnings/edit.html", warning=warning, form=form)
|
||||
@@ -1,100 +1,21 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app.models import *
|
||||
from app.utils import is_package_page
|
||||
from app.querybuilder import QueryBuilder
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("api", __name__)
|
||||
|
||||
@bp.route("/api/packages/")
|
||||
def packages():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildPackageQuery()
|
||||
ver = qb.getMinetestVersion()
|
||||
|
||||
pkgs = [package.getAsDictionaryShort(current_app.config["BASE_URL"], version=ver) \
|
||||
for package in query.all()]
|
||||
return jsonify(pkgs)
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/")
|
||||
@is_package_page
|
||||
def package(package):
|
||||
return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/dependencies/")
|
||||
@is_package_page
|
||||
def package_dependencies(package):
|
||||
ret = []
|
||||
|
||||
for dep in package.dependencies:
|
||||
name = None
|
||||
fulfilled_by = None
|
||||
|
||||
if dep.package:
|
||||
name = dep.package.name
|
||||
fulfilled_by = [ dep.package.getAsDictionaryKey() ]
|
||||
|
||||
elif dep.meta_package:
|
||||
name = dep.meta_package.name
|
||||
fulfilled_by = [ pkg.getAsDictionaryKey() for pkg in dep.meta_package.packages]
|
||||
|
||||
else:
|
||||
raise "Malformed dependency"
|
||||
|
||||
ret.append({
|
||||
"name": name,
|
||||
"is_optional": dep.optional,
|
||||
"packages": fulfilled_by
|
||||
})
|
||||
|
||||
return jsonify(ret)
|
||||
|
||||
|
||||
@bp.route("/api/topics/")
|
||||
def topics():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildTopicQuery(show_added=True)
|
||||
return jsonify([t.getAsDictionary() for t in query.all()])
|
||||
|
||||
|
||||
@bp.route("/api/topic_discard/", methods=["POST"])
|
||||
@login_required
|
||||
def topic_set_discard():
|
||||
tid = request.args.get("tid")
|
||||
discard = request.args.get("discard")
|
||||
if tid is None or discard is None:
|
||||
abort(400)
|
||||
|
||||
topic = ForumTopic.query.get(tid)
|
||||
if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
|
||||
abort(403)
|
||||
|
||||
topic.discarded = discard == "true"
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(topic.getAsDictionary())
|
||||
|
||||
|
||||
@bp.route("/api/minetest_versions/")
|
||||
def versions():
|
||||
return jsonify([{ "name": rel.name, "protocol_version": rel.protocol }\
|
||||
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
|
||||
from . import tokens, endpoints
|
||||
|
||||
46
app/blueprints/api/auth.py
Normal file
46
app/blueprints/api/auth.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2019 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask import request, abort
|
||||
|
||||
from app.models import APIToken
|
||||
from .support import error
|
||||
|
||||
|
||||
def is_api_authd(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
token = None
|
||||
|
||||
value = request.headers.get("authorization")
|
||||
if value is None:
|
||||
pass
|
||||
elif value[0:7].lower() == "bearer ":
|
||||
access_token = value[7:]
|
||||
if len(access_token) < 10:
|
||||
error(400, "API token is too short")
|
||||
|
||||
token = APIToken.query.filter_by(access_token=access_token).first()
|
||||
if token is None:
|
||||
error(403, "Unknown API token")
|
||||
else:
|
||||
abort(403, "Unsupported authentication method")
|
||||
|
||||
return f(token=token, *args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
370
app/blueprints/api/endpoints.py
Normal file
370
app/blueprints/api/endpoints.py
Normal file
@@ -0,0 +1,370 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import request, jsonify, current_app, abort
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
from app import csrf
|
||||
from app.utils.markdown import render_markdown
|
||||
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, ForumTopic, MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.utils import is_package_page
|
||||
from . import bp
|
||||
from .auth import is_api_authd
|
||||
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, api_order_screenshots, api_edit_package
|
||||
|
||||
|
||||
@bp.route("/api/packages/")
|
||||
def packages():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildPackageQuery()
|
||||
|
||||
if request.args.get("fmt") == "keys":
|
||||
return jsonify([package.getAsDictionaryKey() for package in query.all()])
|
||||
|
||||
pkgs = qb.convertToDictionary(query.all())
|
||||
if "engine_version" in request.args or "protocol_version" in request.args:
|
||||
pkgs = [package for package in pkgs if package.get("release")]
|
||||
return jsonify(pkgs)
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/")
|
||||
@is_package_page
|
||||
def package(package):
|
||||
return jsonify(package.getAsDictionary(current_app.config["BASE_URL"]))
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/", methods=["PUT"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
def edit_package(token, package):
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
return api_edit_package(token, package, request.json)
|
||||
|
||||
|
||||
def resolve_package_deps(out, package, only_hard):
|
||||
id = package.getId()
|
||||
if id in out:
|
||||
return
|
||||
|
||||
ret = []
|
||||
out[id] = ret
|
||||
|
||||
for dep in package.dependencies:
|
||||
if only_hard and dep.optional:
|
||||
continue
|
||||
|
||||
if dep.package:
|
||||
name = dep.package.name
|
||||
fulfilled_by = [ dep.package.getId() ]
|
||||
resolve_package_deps(out, dep.package, only_hard)
|
||||
|
||||
elif dep.meta_package:
|
||||
name = dep.meta_package.name
|
||||
fulfilled_by = [ pkg.getId() for pkg in dep.meta_package.packages]
|
||||
# TODO: resolve most likely candidate
|
||||
|
||||
else:
|
||||
raise Exception("Malformed dependency")
|
||||
|
||||
ret.append({
|
||||
"name": name,
|
||||
"is_optional": dep.optional,
|
||||
"packages": fulfilled_by
|
||||
})
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/dependencies/")
|
||||
@is_package_page
|
||||
def package_dependencies(package):
|
||||
only_hard = request.args.get("only_hard")
|
||||
|
||||
out = {}
|
||||
resolve_package_deps(out, package, only_hard)
|
||||
|
||||
return jsonify(out)
|
||||
|
||||
|
||||
@bp.route("/api/topics/")
|
||||
def topics():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildTopicQuery(show_added=True)
|
||||
return jsonify([t.getAsDictionary() for t in query.all()])
|
||||
|
||||
|
||||
@bp.route("/api/topic_discard/", methods=["POST"])
|
||||
@login_required
|
||||
def topic_set_discard():
|
||||
tid = request.args.get("tid")
|
||||
discard = request.args.get("discard")
|
||||
if tid is None or discard is None:
|
||||
abort(400)
|
||||
|
||||
topic = ForumTopic.query.get(tid)
|
||||
if not topic.checkPerm(current_user, Permission.TOPIC_DISCARD):
|
||||
abort(403)
|
||||
|
||||
topic.discarded = discard == "true"
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(topic.getAsDictionary())
|
||||
|
||||
|
||||
@bp.route("/api/whoami/")
|
||||
@is_api_authd
|
||||
def whoami(token):
|
||||
if token is None:
|
||||
return jsonify({ "is_authenticated": False, "username": None })
|
||||
else:
|
||||
return jsonify({ "is_authenticated": True, "username": token.owner.username })
|
||||
|
||||
|
||||
@bp.route("/api/markdown/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def markdown():
|
||||
return render_markdown(request.data.decode("utf-8"))
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/releases/")
|
||||
@is_package_page
|
||||
def list_releases(package):
|
||||
return jsonify([ rel.getAsDictionary() for rel in package.releases.all() ])
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/releases/new/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
def create_release(token, package):
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.APPROVE_RELEASE):
|
||||
error(403, "You do not have the permission to approve releases")
|
||||
|
||||
data = request.json or request.form
|
||||
if "title" not in data:
|
||||
error(400, "Title is required in the POST data")
|
||||
|
||||
if data.get("method") == "git":
|
||||
for option in ["method", "ref"]:
|
||||
if option not in data:
|
||||
error(400, option + " is required in the POST data")
|
||||
|
||||
return api_create_vcs_release(token, package, data["title"], data["ref"])
|
||||
|
||||
elif request.files:
|
||||
file = request.files.get("file")
|
||||
if file is None:
|
||||
error(400, "Missing 'file' in multipart body")
|
||||
|
||||
return api_create_zip_release(token, package, data["title"], file)
|
||||
|
||||
else:
|
||||
error(400, "Unknown release-creation method. Specify the method or provide a file.")
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/releases/<int:id>/")
|
||||
@is_package_page
|
||||
def release(package: Package, id: int):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
error(404, "Release not found")
|
||||
|
||||
return jsonify(release.getAsDictionary())
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/releases/<int:id>/", methods=["DELETE"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
def delete_release(token: APIToken, package: Package, id: int):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
error(404, "Release not found")
|
||||
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
if not release.checkPerm(token.owner, Permission.DELETE_RELEASE):
|
||||
error(403, "Unable to delete the release, make sure there's a newer release available")
|
||||
|
||||
db.session.delete(release)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/")
|
||||
@is_package_page
|
||||
def list_screenshots(package):
|
||||
screenshots = package.screenshots.all()
|
||||
return jsonify([ss.getAsDictionary(current_app.config["BASE_URL"]) for ss in screenshots])
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/new/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
def create_screenshot(token: APIToken, package: Package):
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
|
||||
error(403, "You do not have the permission to create screenshots")
|
||||
|
||||
data = request.form
|
||||
if "title" not in data:
|
||||
error(400, "Title is required in the POST data")
|
||||
|
||||
file = request.files.get("file")
|
||||
if file is None:
|
||||
error(400, "Missing 'file' in multipart body")
|
||||
|
||||
return api_create_screenshot(token, package, data["title"], file)
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/")
|
||||
@is_package_page
|
||||
def screenshot(package, id):
|
||||
ss = PackageScreenshot.query.get(id)
|
||||
if ss is None or ss.package != package:
|
||||
error(404, "Screenshot not found")
|
||||
|
||||
return jsonify(ss.getAsDictionary(current_app.config["BASE_URL"]))
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/", methods=["DELETE"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
def delete_screenshot(token: APIToken, package: Package, id: int):
|
||||
ss = PackageScreenshot.query.get(id)
|
||||
if ss is None or ss.package != package:
|
||||
error(404, "Screenshot not found")
|
||||
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
|
||||
error(403, "You do not have the permission to delete screenshots")
|
||||
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
if package.cover_image == ss:
|
||||
package.cover_image = None
|
||||
db.session.merge(package)
|
||||
|
||||
db.session.delete(ss)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({ "success": True })
|
||||
|
||||
|
||||
@bp.route("/api/packages/<author>/<name>/screenshots/order/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
@is_package_page
|
||||
@is_api_authd
|
||||
def order_screenshots(token: APIToken, package: Package):
|
||||
if not token:
|
||||
error(401, "Authentication needed")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
|
||||
error(403, "You do not have the permission to delete screenshots")
|
||||
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
json = request.json
|
||||
if json is None or not isinstance(json, list):
|
||||
error(400, "Expected order body to be array")
|
||||
|
||||
return api_order_screenshots(token, package, request.json)
|
||||
|
||||
|
||||
@bp.route("/api/scores/")
|
||||
def package_scores():
|
||||
qb = QueryBuilder(request.args)
|
||||
query = qb.buildPackageQuery()
|
||||
|
||||
pkgs = [package.getScoreDict() for package in query.all()]
|
||||
return jsonify(pkgs)
|
||||
|
||||
|
||||
@bp.route("/api/tags/")
|
||||
def tags():
|
||||
return jsonify([tag.getAsDictionary() for tag in Tag.query.all() ])
|
||||
|
||||
|
||||
@bp.route("/api/content_warnings/")
|
||||
def content_warnings():
|
||||
return jsonify([warning.getAsDictionary() for warning in ContentWarning.query.all() ])
|
||||
|
||||
|
||||
@bp.route("/api/licenses/")
|
||||
def licenses():
|
||||
return jsonify([ { "name": license.name, "is_foss": license.is_foss } \
|
||||
for license in License.query.order_by(db.asc(License.name)).all() ])
|
||||
|
||||
|
||||
@bp.route("/api/homepage/")
|
||||
def homepage():
|
||||
query = Package.query.filter_by(state=PackageState.APPROVED)
|
||||
count = query.count()
|
||||
|
||||
new = query.order_by(db.desc(Package.approved_at)).limit(4).all()
|
||||
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
|
||||
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all()
|
||||
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(8).all()
|
||||
high_reviewed = query.order_by(db.desc(Package.score - Package.score_downloads)) \
|
||||
.filter(Package.reviews.any()).limit(4).all()
|
||||
|
||||
updated = db.session.query(Package).select_from(PackageRelease).join(Package) \
|
||||
.filter_by(state=PackageState.APPROVED) \
|
||||
.order_by(db.desc(PackageRelease.releaseDate)) \
|
||||
.limit(20).all()
|
||||
updated = updated[:4]
|
||||
|
||||
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
|
||||
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
|
||||
|
||||
def mapPackages(packages):
|
||||
return [pkg.getAsDictionaryKey() for pkg in packages]
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"downloads": downloads,
|
||||
"new": mapPackages(new),
|
||||
"updated": mapPackages(updated),
|
||||
"pop_mod": mapPackages(pop_mod),
|
||||
"pop_txp": mapPackages(pop_txp),
|
||||
"pop_game": mapPackages(pop_gam),
|
||||
"high_reviewed": mapPackages(high_reviewed)
|
||||
}
|
||||
|
||||
|
||||
@bp.route("/api/minetest_versions/")
|
||||
def versions():
|
||||
return jsonify([rel.getAsDictionary() \
|
||||
for rel in MinetestRelease.query.all() if rel.getActual() is not None])
|
||||
108
app/blueprints/api/support.py
Normal file
108
app/blueprints/api/support.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import jsonify, abort, make_response, url_for, current_app
|
||||
|
||||
from app.logic.packages import do_edit_package
|
||||
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
|
||||
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
|
||||
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
|
||||
|
||||
|
||||
def error(code: int, msg: str):
|
||||
abort(make_response(jsonify({ "success": False, "error": msg }), code))
|
||||
|
||||
# Catches LogicErrors and aborts with JSON error
|
||||
def guard(f):
|
||||
def ret(*args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except LogicError as e:
|
||||
error(e.code, e.message)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def api_create_vcs_release(token: APIToken, package: Package, title: str, ref: str,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
reason += ", token=" + token.name
|
||||
|
||||
rel = guard(do_create_vcs_release)(token.owner, package, title, ref, min_v, max_v, reason)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"task": url_for("tasks.check", id=rel.task_id),
|
||||
"release": rel.getAsDictionary()
|
||||
})
|
||||
|
||||
|
||||
def api_create_zip_release(token: APIToken, package: Package, title: str, file,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
reason += ", token=" + token.name
|
||||
|
||||
rel = guard(do_create_zip_release)(token.owner, package, title, file, min_v, max_v, reason)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"task": url_for("tasks.check", id=rel.task_id),
|
||||
"release": rel.getAsDictionary()
|
||||
})
|
||||
|
||||
|
||||
def api_create_screenshot(token: APIToken, package: Package, title: str, file, reason="API"):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
reason += ", token=" + token.name
|
||||
|
||||
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, reason)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"screenshot": ss.getAsDictionary()
|
||||
})
|
||||
|
||||
|
||||
def api_order_screenshots(token: APIToken, package: Package, order: [any]):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
guard(do_order_screenshots)(token.owner, package, order)
|
||||
|
||||
return jsonify({
|
||||
"success": True
|
||||
})
|
||||
|
||||
|
||||
def api_edit_package(token: APIToken, package: Package, data: dict, reason: str = "API"):
|
||||
if not token.canOperateOnPackage(package):
|
||||
error(403, "API token does not have access to the package")
|
||||
|
||||
reason += ", token=" + token.name
|
||||
|
||||
package = guard(do_edit_package)(token.owner, package, False, data, reason)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"package": package.getAsDictionary(current_app.config["BASE_URL"])
|
||||
})
|
||||
150
app/blueprints/api/tokens.py
Normal file
150
app/blueprints/api/tokens.py
Normal file
@@ -0,0 +1,150 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import render_template, redirect, request, session, url_for, abort
|
||||
from flask_login import login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import db, User, APIToken, Package, Permission
|
||||
from app.utils import randomString
|
||||
from . import bp
|
||||
from ..users.settings import get_setting_tabs
|
||||
|
||||
|
||||
class CreateAPIToken(FlaskForm):
|
||||
name = StringField("Name", [InputRequired(), Length(1, 30)])
|
||||
package = QuerySelectField("Limit to package", allow_blank=True,
|
||||
get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/user/tokens/")
|
||||
@login_required
|
||||
def list_tokens_redirect():
|
||||
return redirect(url_for("api.list_tokens", username=current_user.username))
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/")
|
||||
@login_required
|
||||
def list_tokens(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
return render_template("api/list_tokens.html", user=user, tabs=get_setting_tabs(user), current_tab="api_tokens")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/new/", methods=["GET", "POST"])
|
||||
@bp.route("/users/<username>/tokens/<int:id>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_edit_token(username, id=None):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
is_new = id is None
|
||||
|
||||
token = None
|
||||
access_token = None
|
||||
if not is_new:
|
||||
token = APIToken.query.get(id)
|
||||
if token is None:
|
||||
abort(404)
|
||||
elif token.owner != user:
|
||||
abort(403)
|
||||
|
||||
access_token = session.pop("token_" + str(token.id), None)
|
||||
|
||||
form = CreateAPIToken(formdata=request.form, obj=token)
|
||||
form.package.query_factory = lambda: user.maintained_packages.all()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if is_new:
|
||||
token = APIToken()
|
||||
token.owner = user
|
||||
token.access_token = randomString(32)
|
||||
|
||||
form.populate_obj(token)
|
||||
db.session.add(token)
|
||||
db.session.commit() # save
|
||||
|
||||
if is_new:
|
||||
# Store token so it can be shown in the edit page
|
||||
session["token_" + str(token.id)] = token.access_token
|
||||
|
||||
return redirect(url_for("api.create_edit_token", username=username, id=token.id))
|
||||
|
||||
return render_template("api/create_edit_token.html", user=user, form=form, token=token, access_token=access_token)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/<int:id>/reset/", methods=["POST"])
|
||||
@login_required
|
||||
def reset_token(username, id):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
token = APIToken.query.get(id)
|
||||
if token is None:
|
||||
abort(404)
|
||||
elif token.owner != user:
|
||||
abort(403)
|
||||
|
||||
token.access_token = randomString(32)
|
||||
|
||||
db.session.commit() # save
|
||||
|
||||
# Store token so it can be shown in the edit page
|
||||
session["token_" + str(token.id)] = token.access_token
|
||||
|
||||
return redirect(url_for("api.create_edit_token", username=username, id=token.id))
|
||||
|
||||
|
||||
@bp.route("/users/<username>/tokens/<int:id>/delete/", methods=["POST"])
|
||||
@login_required
|
||||
def delete_token(username, id):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CREATE_TOKEN):
|
||||
abort(403)
|
||||
|
||||
is_new = id is None
|
||||
|
||||
token = APIToken.query.get(id)
|
||||
if token is None:
|
||||
abort(404)
|
||||
elif token.owner != user:
|
||||
abort(403)
|
||||
|
||||
db.session.delete(token)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("api.list_tokens", username=username))
|
||||
152
app/blueprints/github/__init__.py
Normal file
152
app/blueprints/github/__init__.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("github", __name__)
|
||||
|
||||
from flask import redirect, url_for, request, flash, jsonify, current_app
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func, or_, and_
|
||||
from app import github, csrf
|
||||
from app.models import db, User, APIToken, Package, Permission, AuditSeverity
|
||||
from app.utils import abs_url_for, addAuditLog, login_user_set_active
|
||||
from app.blueprints.api.support import error, api_create_vcs_release
|
||||
import hmac, requests
|
||||
|
||||
@bp.route("/github/start/")
|
||||
def start():
|
||||
return github.authorize("", redirect_uri=abs_url_for("github.callback"))
|
||||
|
||||
@bp.route("/github/view/")
|
||||
def view_permissions():
|
||||
url = "https://github.com/settings/connections/applications/" + \
|
||||
current_app.config["GITHUB_CLIENT_ID"]
|
||||
return redirect(url)
|
||||
|
||||
@bp.route("/github/callback/")
|
||||
@github.authorized_handler
|
||||
def callback(oauth_token):
|
||||
next_url = request.args.get("next")
|
||||
if oauth_token is None:
|
||||
flash("Authorization failed [err=gh-oauth-login-failed]", "danger")
|
||||
return redirect(url_for("users.login"))
|
||||
|
||||
# Get Github username
|
||||
url = "https://api.github.com/user"
|
||||
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
|
||||
username = r.json()["login"]
|
||||
|
||||
# Get user by github username
|
||||
userByGithub = User.query.filter(func.lower(User.github_username) == func.lower(username)).first()
|
||||
|
||||
# If logged in, connect
|
||||
if current_user and current_user.is_authenticated:
|
||||
if userByGithub is None:
|
||||
current_user.github_username = username
|
||||
db.session.commit()
|
||||
flash("Linked github to account", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
else:
|
||||
flash("Github account is already associated with another user", "danger")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
# If not logged in, log in
|
||||
else:
|
||||
if userByGithub is None:
|
||||
flash("Unable to find an account for that Github user", "danger")
|
||||
return redirect(url_for("users.claim_forums"))
|
||||
elif login_user_set_active(userByGithub, remember=True):
|
||||
addAuditLog(AuditSeverity.USER, userByGithub, "Logged in using GitHub OAuth",
|
||||
url_for("users.profile", username=userByGithub.username))
|
||||
db.session.commit()
|
||||
|
||||
if not current_user.password:
|
||||
return redirect(next_url or url_for("users.set_password", optional=True))
|
||||
else:
|
||||
return redirect(next_url or url_for("homepage.home"))
|
||||
else:
|
||||
flash("Authorization failed [err=gh-login-failed]", "danger")
|
||||
return redirect(url_for("users.login"))
|
||||
|
||||
|
||||
@bp.route("/github/webhook/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def webhook():
|
||||
json = request.json
|
||||
|
||||
# Get package
|
||||
github_url = "github.com/" + json["repository"]["full_name"]
|
||||
package = Package.query.filter(Package.repo.ilike("%{}%".format(github_url))).first()
|
||||
if package is None:
|
||||
return error(400, "Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(github_url))
|
||||
|
||||
# Get all tokens for package
|
||||
tokens_query = APIToken.query.filter(or_(APIToken.package==package,
|
||||
and_(APIToken.package==None, APIToken.owner==package.author)))
|
||||
|
||||
possible_tokens = tokens_query.all()
|
||||
actual_token = None
|
||||
|
||||
#
|
||||
# Check signature
|
||||
#
|
||||
|
||||
header_signature = request.headers.get('X-Hub-Signature')
|
||||
if header_signature is None:
|
||||
return error(403, "Expected payload signature")
|
||||
|
||||
sha_name, signature = header_signature.split('=')
|
||||
if sha_name != 'sha1':
|
||||
return error(403, "Expected SHA1 payload signature")
|
||||
|
||||
for token in possible_tokens:
|
||||
mac = hmac.new(token.access_token.encode("utf-8"), msg=request.data, digestmod='sha1')
|
||||
|
||||
if hmac.compare_digest(str(mac.hexdigest()), signature):
|
||||
actual_token = token
|
||||
break
|
||||
|
||||
if actual_token is None:
|
||||
return error(403, "Invalid authentication, couldn't validate API token")
|
||||
|
||||
if not package.checkPerm(actual_token.owner, Permission.APPROVE_RELEASE):
|
||||
return error(403, "You do not have the permission to approve releases")
|
||||
|
||||
#
|
||||
# Check event
|
||||
#
|
||||
|
||||
event = request.headers.get("X-GitHub-Event")
|
||||
if event == "push":
|
||||
ref = json["after"]
|
||||
title = json["head_commit"]["message"].partition("\n")[0]
|
||||
elif event == "create" and json["ref_type"] == "tag":
|
||||
ref = json["ref"]
|
||||
title = ref
|
||||
elif event == "ping":
|
||||
return jsonify({ "success": True, "message": "Ping successful" })
|
||||
else:
|
||||
return error(400, "Unsupported event. Only 'push', `create:tag`, and 'ping' are supported.")
|
||||
|
||||
#
|
||||
# Perform release
|
||||
#
|
||||
|
||||
if package.releases.filter_by(commit_hash=ref).count() > 0:
|
||||
return
|
||||
|
||||
return api_create_vcs_release(actual_token, package, title, ref, reason="Webhook")
|
||||
78
app/blueprints/gitlab/__init__.py
Normal file
78
app/blueprints/gitlab/__init__.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint, request
|
||||
|
||||
bp = Blueprint("gitlab", __name__)
|
||||
|
||||
from app import csrf
|
||||
from app.models import Package, APIToken, Permission
|
||||
from app.blueprints.api.support import error, api_create_vcs_release
|
||||
|
||||
|
||||
def webhook_impl():
|
||||
json = request.json
|
||||
|
||||
# Get package
|
||||
gitlab_url = json["project"]["web_url"].replace("https://", "").replace("http://", "")
|
||||
package = Package.query.filter(Package.repo.ilike("%{}%".format(gitlab_url))).first()
|
||||
if package is None:
|
||||
return error(400,
|
||||
"Could not find package, did you set the VCS repo in CDB correctly? Expected {}".format(gitlab_url))
|
||||
|
||||
# Get all tokens for package
|
||||
secret = request.headers.get("X-Gitlab-Token")
|
||||
if secret is None:
|
||||
return error(403, "Token required")
|
||||
|
||||
token = APIToken.query.filter_by(access_token=secret).first()
|
||||
if token is None:
|
||||
return error(403, "Invalid authentication")
|
||||
|
||||
if not package.checkPerm(token.owner, Permission.APPROVE_RELEASE):
|
||||
return error(403, "You do not have the permission to approve releases")
|
||||
|
||||
#
|
||||
# Check event
|
||||
#
|
||||
|
||||
event = json["event_name"]
|
||||
if event == "push":
|
||||
ref = json["after"]
|
||||
title = ref[:5]
|
||||
elif event == "tag_push":
|
||||
ref = json["ref"]
|
||||
title = ref.replace("refs/tags/", "")
|
||||
else:
|
||||
return error(400, "Unsupported event. Only 'push' and 'tag_push' are supported.")
|
||||
|
||||
#
|
||||
# Perform release
|
||||
#
|
||||
|
||||
if package.releases.filter_by(commit_hash=ref).count() > 0:
|
||||
return
|
||||
|
||||
return api_create_vcs_release(token, package, title, ref, reason="Webhook")
|
||||
|
||||
|
||||
@bp.route("/gitlab/webhook/", methods=["POST"])
|
||||
@csrf.exempt
|
||||
def webhook():
|
||||
try:
|
||||
return webhook_impl()
|
||||
except KeyError as err:
|
||||
return error(400, "Missing field: {}".format(err.args[0]))
|
||||
@@ -4,17 +4,40 @@ bp = Blueprint("homepage", __name__)
|
||||
|
||||
from app.models import *
|
||||
import flask_menu as menu
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
@bp.route("/")
|
||||
@menu.register_menu(bp, ".", "Home")
|
||||
def home():
|
||||
query = Package.query.filter_by(approved=True, soft_deleted=False)
|
||||
def join(query):
|
||||
return query.options(
|
||||
joinedload(Package.license),
|
||||
joinedload(Package.media_license))
|
||||
|
||||
query = Package.query.filter_by(state=PackageState.APPROVED)
|
||||
count = query.count()
|
||||
new = query.order_by(db.desc(Package.created_at)).limit(8).all()
|
||||
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
|
||||
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(4).all()
|
||||
pop_txp = query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score)).limit(4).all()
|
||||
downloads = db.session.query(func.sum(PackageRelease.downloads)).first()[0]
|
||||
return render_template("index.html", count=count, downloads=downloads, \
|
||||
new=new, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam)
|
||||
|
||||
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
|
||||
pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all()
|
||||
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all()
|
||||
pop_txp = join(query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score))).limit(8).all()
|
||||
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
|
||||
.filter(Package.reviews.any()).limit(4).all()
|
||||
|
||||
updated = db.session.query(Package).select_from(PackageRelease).join(Package) \
|
||||
.filter_by(state=PackageState.APPROVED) \
|
||||
.order_by(db.desc(PackageRelease.releaseDate)) \
|
||||
.limit(20).all()
|
||||
updated = updated[:4]
|
||||
|
||||
reviews = PackageReview.query.filter_by(recommends=True).order_by(db.desc(PackageReview.created_at)).limit(5).all()
|
||||
|
||||
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
|
||||
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
|
||||
|
||||
tags = db.session.query(func.count(Tags.c.tag_id), Tag) \
|
||||
.select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all()
|
||||
|
||||
return render_template("index.html", count=count, downloads=downloads, tags=tags,
|
||||
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed, reviews=reviews)
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from sqlalchemy import func
|
||||
from app.models import MetaPackage, Package, db, Dependency, PackageState, ForumTopic
|
||||
|
||||
bp = Blueprint("metapackages", __name__)
|
||||
|
||||
from flask_user import *
|
||||
from app.models import *
|
||||
|
||||
@bp.route("/metapackages/")
|
||||
def list_all():
|
||||
mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
|
||||
return render_template("meta/list.html", mpackages=mpackages)
|
||||
mpackages = db.session.query(MetaPackage, func.count(Package.id)) \
|
||||
.select_from(MetaPackage).outerjoin(MetaPackage.packages) \
|
||||
.order_by(db.asc(MetaPackage.name)) \
|
||||
.group_by(MetaPackage.id).all()
|
||||
return render_template("metapackages/list.html", mpackages=mpackages)
|
||||
|
||||
|
||||
@bp.route("/metapackages/<name>/")
|
||||
def view(name):
|
||||
@@ -33,4 +37,29 @@ def view(name):
|
||||
if mpackage is None:
|
||||
abort(404)
|
||||
|
||||
return render_template("meta/view.html", mpackage=mpackage)
|
||||
dependers = db.session.query(Package) \
|
||||
.select_from(MetaPackage) \
|
||||
.filter(MetaPackage.name==name) \
|
||||
.join(MetaPackage.dependencies) \
|
||||
.join(Dependency.depender) \
|
||||
.filter(Dependency.optional==False, Package.state==PackageState.APPROVED) \
|
||||
.all()
|
||||
|
||||
optional_dependers = db.session.query(Package) \
|
||||
.select_from(MetaPackage) \
|
||||
.filter(MetaPackage.name==name) \
|
||||
.join(MetaPackage.dependencies) \
|
||||
.join(Dependency.depender) \
|
||||
.filter(Dependency.optional==True, Package.state==PackageState.APPROVED) \
|
||||
.all()
|
||||
|
||||
similar_topics = None
|
||||
if mpackage.packages.filter_by(state=PackageState.APPROVED).count() == 0:
|
||||
similar_topics = ForumTopic.query \
|
||||
.filter_by(name=name) \
|
||||
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||
.all()
|
||||
|
||||
return render_template("metapackages/view.html", mpackage=mpackage,
|
||||
dependers=dependers, optional_dependers=optional_dependers,
|
||||
similar_topics=similar_topics)
|
||||
|
||||
74
app/blueprints/metrics/__init__.py
Normal file
74
app/blueprints/metrics/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint, make_response
|
||||
from sqlalchemy.sql.expression import func
|
||||
|
||||
from app.models import Package, db, User, UserRank, PackageState
|
||||
|
||||
bp = Blueprint("metrics", __name__)
|
||||
|
||||
def generate_metrics(full=False):
|
||||
def write_single_stat(name, help, type, value):
|
||||
fmt = "# HELP {name} {help}\n# TYPE {name} {type}\n{name} {value}\n\n"
|
||||
|
||||
return fmt.format(name=name, help=help, type=type, value=value)
|
||||
|
||||
def gen_labels(labels):
|
||||
pieces = [key + "=" + str(val) for key, val in labels.items()]
|
||||
return ",".join(pieces)
|
||||
|
||||
|
||||
def write_array_stat(name, help, type, data):
|
||||
ret = "# HELP {name} {help}\n# TYPE {name} {type}\n" \
|
||||
.format(name=name, help=help, type=type)
|
||||
|
||||
for entry in data:
|
||||
assert(len(entry) == 2)
|
||||
ret += "{name}{{{labels}}} {value}\n" \
|
||||
.format(name=name, labels=gen_labels(entry[0]), value=entry[1])
|
||||
|
||||
return ret + "\n"
|
||||
|
||||
downloads_result = db.session.query(func.sum(Package.downloads)).one_or_none()
|
||||
downloads = 0 if not downloads_result or not downloads_result[0] else downloads_result[0]
|
||||
|
||||
packages = Package.query.filter_by(state=PackageState.APPROVED).count()
|
||||
users = User.query.filter(User.rank != UserRank.NOT_JOINED).count()
|
||||
|
||||
ret = ""
|
||||
ret += write_single_stat("contentdb_packages", "Total packages", "counter", packages)
|
||||
ret += write_single_stat("contentdb_users", "Number of registered users", "counter", users)
|
||||
ret += write_single_stat("contentdb_downloads", "Total downloads", "counter", downloads)
|
||||
|
||||
if full:
|
||||
scores = Package.query.join(User).with_entities(User.username, Package.name, Package.score) \
|
||||
.filter(Package.state==PackageState.APPROVED).all()
|
||||
|
||||
ret += write_array_stat("contentdb_package_score", "Package score", "gauge",
|
||||
[({ "author": score[0], "name": score[1] }, score[2]) for score in scores])
|
||||
else:
|
||||
score_result = db.session.query(func.sum(Package.score)).one_or_none()
|
||||
score = 0 if not score_result or not score_result[0] else score_result[0]
|
||||
ret += write_single_stat("contentdb_score", "Total package score", "gauge", score)
|
||||
|
||||
return ret
|
||||
|
||||
@bp.route("/metrics")
|
||||
def metrics():
|
||||
response = make_response(generate_metrics(), 200)
|
||||
response.mimetype = "text/plain"
|
||||
return response
|
||||
@@ -1,34 +1,49 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import Blueprint, render_template, redirect
|
||||
from flask_user import current_user, login_required
|
||||
from app.models import db
|
||||
from flask import Blueprint, render_template, redirect, url_for
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import or_, desc
|
||||
|
||||
from app.models import db, Notification, NotificationType
|
||||
|
||||
bp = Blueprint("notifications", __name__)
|
||||
|
||||
|
||||
@bp.route("/notifications/")
|
||||
@login_required
|
||||
def list_all():
|
||||
return render_template("notifications/list.html")
|
||||
notifications = Notification.query.filter(Notification.user == current_user,
|
||||
Notification.type != NotificationType.EDITOR_ALERT, Notification.type != NotificationType.EDITOR_MISC) \
|
||||
.order_by(desc(Notification.created_at)) \
|
||||
.all()
|
||||
|
||||
editor_notifications = Notification.query.filter(Notification.user == current_user,
|
||||
or_(Notification.type == NotificationType.EDITOR_ALERT, Notification.type == NotificationType.EDITOR_MISC)) \
|
||||
.order_by(desc(Notification.created_at)) \
|
||||
.all()
|
||||
|
||||
return render_template("notifications/list.html",
|
||||
notifications=notifications, editor_notifications=editor_notifications)
|
||||
|
||||
|
||||
@bp.route("/notifications/clear/", methods=["POST"])
|
||||
@login_required
|
||||
def clear():
|
||||
current_user.notifications.clear()
|
||||
Notification.query.filter_by(user=current_user).delete()
|
||||
db.session.commit()
|
||||
return redirect(url_for("notifications.list_all"))
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("packages", __name__)
|
||||
|
||||
from . import packages, screenshots, releases
|
||||
from . import packages, screenshots, releases, reviews
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from app import app
|
||||
from app.models import *
|
||||
|
||||
from app.utils import *
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
|
||||
|
||||
from . import PackageForm
|
||||
|
||||
|
||||
class EditRequestForm(PackageForm):
|
||||
edit_title = StringField("Edit Title", [InputRequired(), Length(1, 100)])
|
||||
edit_desc = TextField("Edit Description", [Optional()])
|
||||
|
||||
@app.route("/packages/<author>/<name>/requests/new/", methods=["GET","POST"])
|
||||
@app.route("/packages/<author>/<name>/requests/<id>/edit/", methods=["GET","POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def create_edit_editrequest_page(package, id=None):
|
||||
edited_package = package
|
||||
|
||||
erequest = None
|
||||
if id is not None:
|
||||
erequest = EditRequest.query.get(id)
|
||||
if erequest.package != package:
|
||||
abort(404)
|
||||
|
||||
if not erequest.checkPerm(current_user, Permission.EDIT_EDITREQUEST):
|
||||
abort(403)
|
||||
|
||||
if erequest.status != 0:
|
||||
flash("Can't edit EditRequest, it has already been merged or rejected", "error")
|
||||
return redirect(erequest.getURL())
|
||||
|
||||
edited_package = Package(package)
|
||||
erequest.applyAll(edited_package)
|
||||
|
||||
form = EditRequestForm(request.form, obj=edited_package)
|
||||
if request.method == "GET":
|
||||
deps = edited_package.dependencies
|
||||
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
|
||||
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
|
||||
form.provides_str.data = MetaPackage.ListToSpec(edited_package.provides)
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
if erequest is None:
|
||||
erequest = EditRequest()
|
||||
erequest.package = package
|
||||
erequest.author = current_user
|
||||
|
||||
erequest.title = form["edit_title"].data
|
||||
erequest.desc = form["edit_desc"].data
|
||||
db.session.add(erequest)
|
||||
|
||||
EditRequestChange.query.filter_by(request=erequest).delete()
|
||||
|
||||
wasChangeMade = False
|
||||
for e in PackagePropertyKey:
|
||||
newValue = form[e.name].data
|
||||
oldValue = getattr(package, e.name)
|
||||
|
||||
newValueComp = newValue
|
||||
oldValueComp = oldValue
|
||||
if type(newValue) is str:
|
||||
newValue = newValue.replace("\r\n", "\n")
|
||||
newValueComp = newValue.strip()
|
||||
oldValueComp = "" if oldValue is None else oldValue.strip()
|
||||
|
||||
if newValueComp != oldValueComp:
|
||||
change = EditRequestChange()
|
||||
change.request = erequest
|
||||
change.key = e
|
||||
change.oldValue = e.convert(oldValue)
|
||||
change.newValue = e.convert(newValue)
|
||||
db.session.add(change)
|
||||
wasChangeMade = True
|
||||
|
||||
if wasChangeMade:
|
||||
msg = "{}: Edit request #{} {}" \
|
||||
.format(package.title, erequest.id, "created" if id is None else "edited")
|
||||
triggerNotif(package.author, current_user, msg, erequest.getURL())
|
||||
triggerNotif(erequest.author, current_user, msg, erequest.getURL())
|
||||
db.session.commit()
|
||||
return redirect(erequest.getURL())
|
||||
else:
|
||||
flash("No changes detected", "warning")
|
||||
elif erequest is not None:
|
||||
form["edit_title"].data = erequest.title
|
||||
form["edit_desc"].data = erequest.desc
|
||||
|
||||
return render_template("packages/editrequest_create_edit.html", package=package, form=form)
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/requests/<id>/")
|
||||
@is_package_page
|
||||
def view_editrequest_page(package, id):
|
||||
erequest = EditRequest.query.get(id)
|
||||
if erequest is None or erequest.package != package:
|
||||
abort(404)
|
||||
|
||||
clearNotifications(erequest.getURL())
|
||||
return render_template("packages/editrequest_view.html", package=package, request=erequest)
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/requests/<id>/approve/", methods=["POST"])
|
||||
@is_package_page
|
||||
def approve_editrequest_page(package, id):
|
||||
if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
|
||||
flash("You don't have permission to do that.", "error")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
erequest = EditRequest.query.get(id)
|
||||
if erequest is None or erequest.package != package:
|
||||
abort(404)
|
||||
|
||||
if erequest.status != 0:
|
||||
flash("Edit request has already been resolved", "error")
|
||||
|
||||
else:
|
||||
erequest.status = 1
|
||||
erequest.applyAll(package)
|
||||
|
||||
msg = "{}: Edit request #{} merged".format(package.title, erequest.id)
|
||||
triggerNotif(erequest.author, current_user, msg, erequest.getURL())
|
||||
triggerNotif(package.author, current_user, msg, erequest.getURL())
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
@app.route("/packages/<author>/<name>/requests/<id>/reject/", methods=["POST"])
|
||||
@is_package_page
|
||||
def reject_editrequest_page(package, id):
|
||||
if not package.checkPerm(current_user, Permission.APPROVE_CHANGES):
|
||||
flash("You don't have permission to do that.", "error")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
erequest = EditRequest.query.get(id)
|
||||
if erequest is None or erequest.package != package:
|
||||
abort(404)
|
||||
|
||||
if erequest.status != 0:
|
||||
flash("Edit request has already been resolved", "error")
|
||||
|
||||
else:
|
||||
erequest.status = 2
|
||||
|
||||
msg = "{}: Edit request #{} rejected".format(package.title, erequest.id)
|
||||
triggerNotif(erequest.author, current_user, msg, erequest.getURL())
|
||||
triggerNotif(package.author, current_user, msg, erequest.getURL())
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
@@ -1,36 +1,40 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import render_template, abort, request, redirect, url_for, flash
|
||||
from flask_user import current_user
|
||||
from urllib.parse import quote as urlescape
|
||||
|
||||
import flask_menu as menu
|
||||
|
||||
from . import bp
|
||||
|
||||
from app.models import *
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.tasks.importtasks import importRepoScreenshot
|
||||
from app.utils import *
|
||||
|
||||
from celery import uuid
|
||||
from flask import render_template, flash
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import or_, func
|
||||
from sqlalchemy.orm import joinedload, subqueryload
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
|
||||
from sqlalchemy import or_
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.rediscache import has_key, set_key
|
||||
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease
|
||||
from app.utils import *
|
||||
from . import bp
|
||||
from ...logic.LogicError import LogicError
|
||||
from ...logic.packages import do_edit_package
|
||||
|
||||
|
||||
@menu.register_menu(bp, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
|
||||
@@ -43,6 +47,26 @@ def list_all():
|
||||
query = qb.buildPackageQuery()
|
||||
title = qb.title
|
||||
|
||||
query = query.options(
|
||||
joinedload(Package.license),
|
||||
joinedload(Package.media_license),
|
||||
subqueryload(Package.tags))
|
||||
|
||||
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
if ip is not None and not is_user_bot():
|
||||
edited = False
|
||||
for tag in qb.tags:
|
||||
edited = True
|
||||
key = "tag/{}/{}".format(ip, tag.name)
|
||||
if not has_key(key):
|
||||
set_key(key, "true")
|
||||
Tag.query.filter_by(id=tag.id).update({
|
||||
"views": Tag.views + 1
|
||||
})
|
||||
|
||||
if edited:
|
||||
db.session.commit()
|
||||
|
||||
if qb.lucky:
|
||||
package = query.first()
|
||||
if package:
|
||||
@@ -59,20 +83,29 @@ def list_all():
|
||||
search = request.args.get("q")
|
||||
type_name = request.args.get("type")
|
||||
|
||||
next_url = url_for("packages.list_all", type=type_name, q=search, page=query.next_num) \
|
||||
if query.has_next else None
|
||||
prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \
|
||||
if query.has_prev else None
|
||||
authors = []
|
||||
if search:
|
||||
authors = User.query \
|
||||
.filter(or_(*[func.lower(User.username) == name.lower().strip() for name in search.split(" ")])) \
|
||||
.all()
|
||||
|
||||
authors = [(author.username, search.lower().replace(author.username.lower(), "")) for author in authors]
|
||||
|
||||
topics = None
|
||||
if qb.search and not query.has_next:
|
||||
qb.show_discarded = True
|
||||
topics = qb.buildTopicQuery().all()
|
||||
|
||||
tags = Tag.query.all()
|
||||
return render_template("packages/list.html", \
|
||||
title=title, packages=query.items, topics=topics, \
|
||||
query=search, tags=tags, type=type_name, \
|
||||
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
|
||||
tags_query = db.session.query(func.count(Tags.c.tag_id), Tag) \
|
||||
.select_from(Tag).join(Tags).join(Package).group_by(Tag.id).order_by(db.asc(Tag.title))
|
||||
tags = qb.filterPackageQuery(tags_query).all()
|
||||
|
||||
selected_tags = set(qb.tags)
|
||||
|
||||
return render_template("packages/list.html",
|
||||
title=title, packages=query.items, pagination=query,
|
||||
query=search, tags=tags, selected_tags=selected_tags, type=type_name,
|
||||
authors=authors, packages_count=query.total, topics=topics)
|
||||
|
||||
|
||||
def getReleases(package):
|
||||
@@ -85,13 +118,11 @@ def getReleases(package):
|
||||
@bp.route("/packages/<author>/<name>/")
|
||||
@is_package_page
|
||||
def view(package):
|
||||
clearNotifications(package.getDetailsURL())
|
||||
|
||||
alternatives = None
|
||||
if package.type == PackageType.MOD:
|
||||
alternatives = Package.query \
|
||||
.filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
|
||||
.filter(Package.id != package.id) \
|
||||
.filter_by(name=package.name, type=PackageType.MOD) \
|
||||
.filter(Package.id != package.id, Package.state!=PackageState.DELETED) \
|
||||
.order_by(db.desc(Package.score)) \
|
||||
.all()
|
||||
|
||||
@@ -108,7 +139,6 @@ def view(package):
|
||||
.all()
|
||||
|
||||
releases = getReleases(package)
|
||||
requests = [r for r in package.requests if r.status == 0]
|
||||
|
||||
review_thread = package.review_thread
|
||||
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
@@ -116,9 +146,9 @@ def view(package):
|
||||
|
||||
topic_error = None
|
||||
topic_error_lvl = "warning"
|
||||
if not package.approved and package.forums is not None:
|
||||
if package.state != PackageState.APPROVED and package.forums is not None:
|
||||
errors = []
|
||||
if Package.query.filter_by(forums=package.forums, soft_deleted=False).count() > 1:
|
||||
if Package.query.filter(Package.forums==package.forums, Package.state!=PackageState.DELETED).count() > 1:
|
||||
errors.append("<b>Error: Another package already uses this forum topic!</b>")
|
||||
topic_error_lvl = "danger"
|
||||
|
||||
@@ -127,27 +157,42 @@ def view(package):
|
||||
if topic.author != package.author:
|
||||
errors.append("<b>Error: Forum topic author doesn't match package author.</b>")
|
||||
topic_error_lvl = "danger"
|
||||
|
||||
if topic.wip:
|
||||
errors.append("Warning: Forum topic is in WIP section, make sure package meets playability standards.")
|
||||
elif package.type != PackageType.TXP:
|
||||
errors.append("Warning: Forum topic not found. This may happen if the topic has only just been created.")
|
||||
|
||||
topic_error = "<br />".join(errors)
|
||||
|
||||
|
||||
threads = Thread.query.filter_by(package_id=package.id)
|
||||
threads = Thread.query.filter_by(package_id=package.id, review_id=None)
|
||||
if not current_user.is_authenticated:
|
||||
threads = threads.filter_by(private=False)
|
||||
elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
|
||||
threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
|
||||
|
||||
has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
|
||||
|
||||
return render_template("packages/view.html", \
|
||||
package=package, releases=releases, requests=requests, \
|
||||
alternatives=alternatives, similar_topics=similar_topics, \
|
||||
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
|
||||
threads=threads.all())
|
||||
return render_template("packages/view.html",
|
||||
package=package, releases=releases,
|
||||
alternatives=alternatives, similar_topics=similar_topics,
|
||||
review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl,
|
||||
threads=threads.all(), has_review=has_review)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/shields/<type>/")
|
||||
@is_package_page
|
||||
def shield(package, type):
|
||||
if type == "title":
|
||||
url = "https://img.shields.io/badge/ContentDB-{}-{}" \
|
||||
.format(urlescape(package.title), urlescape("#375a7f"))
|
||||
elif type == "downloads":
|
||||
#api_url = abs_url_for("api.package", author=package.author.username, name=package.name)
|
||||
api_url = "https://content.minetest.net" + url_for("api.package", author=package.author.username, name=package.name)
|
||||
url = "https://img.shields.io/badge/dynamic/json?color={}&label=ContentDB&query=downloads&suffix=+downloads&url={}" \
|
||||
.format(urlescape("#375a7f"), urlescape(api_url))
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
return redirect(url)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/download/")
|
||||
@@ -160,41 +205,44 @@ def download(package):
|
||||
not "text/html" in request.accept_mimetypes:
|
||||
return "", 204
|
||||
else:
|
||||
flash("No download available.", "error")
|
||||
flash("No download available.", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
else:
|
||||
PackageRelease.query.filter_by(id=release.id).update({
|
||||
"downloads": PackageRelease.downloads + 1
|
||||
})
|
||||
db.session.commit()
|
||||
return redirect(release.getDownloadURL(), code=302)
|
||||
|
||||
return redirect(release.url, code=302)
|
||||
|
||||
def makeLabel(obj):
|
||||
if obj.description:
|
||||
return "{}: {}".format(obj.title, obj.description)
|
||||
else:
|
||||
return obj.title
|
||||
|
||||
class PackageForm(FlaskForm):
|
||||
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
|
||||
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
|
||||
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
|
||||
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
|
||||
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
provides_str = StringField("Provides (mods included in package)", [Optional()])
|
||||
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||
harddep_str = StringField("Hard Dependencies", [Optional()])
|
||||
softdep_str = StringField("Soft Dependencies", [Optional()])
|
||||
repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
||||
submit = SubmitField("Save")
|
||||
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
|
||||
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 100)])
|
||||
name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
|
||||
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
|
||||
|
||||
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=makeLabel)
|
||||
content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=makeLabel)
|
||||
license = QuerySelectField("License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
|
||||
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
|
||||
|
||||
repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
||||
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/packages/new/", methods=["GET", "POST"])
|
||||
@bp.route("/packages/<author>/<name>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_edit(author=None, name=None):
|
||||
package = None
|
||||
form = None
|
||||
if author is None:
|
||||
form = PackageForm(formdata=request.form)
|
||||
author = request.args.get("author")
|
||||
@@ -203,15 +251,17 @@ def create_edit(author=None, name=None):
|
||||
else:
|
||||
author = User.query.filter_by(username=author).first()
|
||||
if author is None:
|
||||
flash("Unable to find that user", "error")
|
||||
flash("Unable to find that user", "danger")
|
||||
return redirect(url_for("packages.create_edit"))
|
||||
|
||||
if not author.checkPerm(current_user, Permission.CHANGE_AUTHOR):
|
||||
flash("Permission denied", "error")
|
||||
flash("Permission denied", "danger")
|
||||
return redirect(url_for("packages.create_edit"))
|
||||
|
||||
else:
|
||||
package = getPackageByInfo(author, name)
|
||||
if package is None:
|
||||
abort(404)
|
||||
if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
@@ -226,108 +276,111 @@ def create_edit(author=None, name=None):
|
||||
form.title.data = request.args.get("title")
|
||||
form.repo.data = request.args.get("repo")
|
||||
form.forums.data = request.args.get("forums")
|
||||
form.license.data = None
|
||||
form.media_license.data = None
|
||||
else:
|
||||
deps = package.dependencies
|
||||
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
|
||||
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
|
||||
form.provides_str.data = MetaPackage.ListToSpec(package.provides)
|
||||
# form.harddep_str.data = ",".join([str(x) for x in package.getSortedHardDependencies() ])
|
||||
# form.softdep_str.data = ",".join([str(x) for x in package.getSortedOptionalDependencies() ])
|
||||
form.tags.data = list(package.tags)
|
||||
form.content_warnings.data = list(package.content_warnings)
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
if request.method == "POST" and form.type.data == PackageType.TXP:
|
||||
form.license.data = form.media_license.data
|
||||
|
||||
if form.validate_on_submit():
|
||||
wasNew = False
|
||||
if not package:
|
||||
package = Package.query.filter_by(name=form["name"].data, author_id=author.id).first()
|
||||
if package is not None:
|
||||
if package.soft_deleted:
|
||||
if package.state == PackageState.READY_FOR_REVIEW:
|
||||
Package.query.filter_by(name=form["name"].data, author_id=author.id).delete()
|
||||
else:
|
||||
flash("Package already exists!", "error")
|
||||
flash("Package already exists!", "danger")
|
||||
return redirect(url_for("packages.create_edit"))
|
||||
|
||||
package = Package()
|
||||
package.author = author
|
||||
package.maintainers.append(author)
|
||||
wasNew = True
|
||||
|
||||
elif package.approved and package.name != form.name.data and \
|
||||
not package.checkPerm(current_user, Permission.CHANGE_NAME):
|
||||
flash("Unable to change package name", "danger")
|
||||
return redirect(url_for("packages.create_edit", author=author, name=name))
|
||||
try:
|
||||
do_edit_package(current_user, package, wasNew, {
|
||||
"type": form.type.data,
|
||||
"title": form.title.data,
|
||||
"name": form.name.data,
|
||||
"short_desc": form.short_desc.data,
|
||||
"tags": form.tags.raw_data,
|
||||
"content_warnings": form.content_warnings.raw_data,
|
||||
"license": form.license.data,
|
||||
"media_license": form.media_license.data,
|
||||
"desc": form.desc.data,
|
||||
"repo": form.repo.data,
|
||||
"website": form.website.data,
|
||||
"issueTracker": form.issueTracker.data,
|
||||
"forums": form.forums.data,
|
||||
})
|
||||
|
||||
else:
|
||||
triggerNotif(package.author, current_user,
|
||||
"{} edited".format(package.title), package.getDetailsURL())
|
||||
if wasNew and package.repo is not None:
|
||||
importRepoScreenshot.delay(package.id)
|
||||
|
||||
form.populate_obj(package) # copy to row
|
||||
next_url = package.getDetailsURL()
|
||||
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
|
||||
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
|
||||
elif wasNew:
|
||||
next_url = package.getSetupReleasesURL()
|
||||
|
||||
if package.type== PackageType.TXP:
|
||||
package.license = package.media_license
|
||||
return redirect(next_url)
|
||||
except LogicError as e:
|
||||
flash(e.message, "danger")
|
||||
|
||||
mpackage_cache = {}
|
||||
package.provides.clear()
|
||||
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
|
||||
for m in mpackages:
|
||||
package.provides.append(m)
|
||||
|
||||
Dependency.query.filter_by(depender=package).delete()
|
||||
deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
|
||||
for dep in deps:
|
||||
dep.optional = False
|
||||
db.session.add(dep)
|
||||
|
||||
deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
|
||||
for dep in deps:
|
||||
dep.optional = True
|
||||
db.session.add(dep)
|
||||
|
||||
if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
|
||||
m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
|
||||
package.provides.append(m)
|
||||
|
||||
package.tags.clear()
|
||||
for tag in form.tags.raw_data:
|
||||
package.tags.append(Tag.query.get(tag))
|
||||
|
||||
db.session.commit() # save
|
||||
|
||||
next_url = package.getDetailsURL()
|
||||
if wasNew and package.repo is not None:
|
||||
task = importRepoScreenshot.delay(package.id)
|
||||
next_url = url_for("tasks.check", id=task.id, r=next_url)
|
||||
|
||||
if wasNew and ("WTFPL" in package.license.name or "WTFPL" in package.media_license.name):
|
||||
next_url = url_for("flatpage", path="help/wtfpl", r=next_url)
|
||||
|
||||
return redirect(next_url)
|
||||
|
||||
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
|
||||
package_query = Package.query.filter_by(state=PackageState.APPROVED)
|
||||
if package is not None:
|
||||
package_query = package_query.filter(Package.id != package.id)
|
||||
|
||||
enableWizard = name is None and request.method != "POST"
|
||||
return render_template("packages/create_edit.html", package=package, \
|
||||
form=form, author=author, enable_wizard=enableWizard, \
|
||||
packages=package_query.all(), \
|
||||
return render_template("packages/create_edit.html", package=package,
|
||||
form=form, author=author, enable_wizard=enableWizard,
|
||||
packages=package_query.all(),
|
||||
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
|
||||
|
||||
@bp.route("/packages/<author>/<name>/approve/", methods=["POST"])
|
||||
|
||||
@bp.route("/packages/<author>/<name>/state/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def approve(package):
|
||||
if not package.checkPerm(current_user, Permission.APPROVE_NEW):
|
||||
flash("You don't have permission to do that.", "error")
|
||||
def move_to_state(package):
|
||||
state = PackageState.get(request.args.get("state"))
|
||||
if state is None:
|
||||
abort(400)
|
||||
|
||||
elif package.approved:
|
||||
flash("Package has already been approved", "error")
|
||||
if not package.canMoveToState(current_user, state):
|
||||
flash("You don't have permission to do that", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
else:
|
||||
package.approved = True
|
||||
package.state = state
|
||||
msg = "Marked {} as {}".format(package.title, state.value)
|
||||
|
||||
if state == PackageState.APPROVED:
|
||||
if not package.approved_at:
|
||||
package.approved_at = datetime.datetime.now()
|
||||
|
||||
screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
|
||||
for s in screenshots:
|
||||
s.approved = True
|
||||
|
||||
triggerNotif(package.author, current_user,
|
||||
"{} approved".format(package.title), package.getDetailsURL())
|
||||
db.session.commit()
|
||||
msg = "Approved {}".format(package.title)
|
||||
|
||||
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.getDetailsURL(), package)
|
||||
severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR
|
||||
addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if package.state == PackageState.CHANGES_NEEDED:
|
||||
flash("Please comment what changes are needed in the review thread", "warning")
|
||||
if package.review_thread:
|
||||
return redirect(package.review_thread.getViewURL())
|
||||
else:
|
||||
return redirect(url_for('threads.new', pid=package.id, title='Package approval comments'))
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
@@ -341,14 +394,15 @@ def remove(package):
|
||||
|
||||
if "delete" in request.form:
|
||||
if not package.checkPerm(current_user, Permission.DELETE_PACKAGE):
|
||||
flash("You don't have permission to do that.", "error")
|
||||
flash("You don't have permission to do that.", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
package.soft_deleted = True
|
||||
package.state = PackageState.DELETED
|
||||
|
||||
url = url_for("users.profile", username=package.author.username)
|
||||
triggerNotif(package.author, current_user,
|
||||
"{} deleted".format(package.title), url)
|
||||
msg = "Deleted {}".format(package.title)
|
||||
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg, url, package)
|
||||
addAuditLog(AuditSeverity.EDITOR, current_user, msg, url)
|
||||
db.session.commit()
|
||||
|
||||
flash("Deleted package", "success")
|
||||
@@ -356,13 +410,15 @@ def remove(package):
|
||||
return redirect(url)
|
||||
elif "unapprove" in request.form:
|
||||
if not package.checkPerm(current_user, Permission.UNAPPROVE_PACKAGE):
|
||||
flash("You don't have permission to do that.", "error")
|
||||
flash("You don't have permission to do that.", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
package.approved = False
|
||||
package.state = PackageState.WIP
|
||||
|
||||
msg = "Unapproved {}".format(package.title)
|
||||
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_APPROVAL, msg, package.getDetailsURL(), package)
|
||||
addAuditLog(AuditSeverity.EDITOR, current_user, msg, package.getDetailsURL(), package)
|
||||
|
||||
triggerNotif(package.author, current_user,
|
||||
"{} deleted".format(package.title), package.getDetailsURL())
|
||||
db.session.commit()
|
||||
|
||||
flash("Unapproved package", "success")
|
||||
@@ -370,3 +426,104 @@ def remove(package):
|
||||
return redirect(package.getDetailsURL())
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
|
||||
|
||||
class PackageMaintainersForm(FlaskForm):
|
||||
maintainers_str = StringField("Maintainers (Comma-separated)", [Optional()])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/edit-maintainers/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def edit_maintainers(package):
|
||||
if not package.checkPerm(current_user, Permission.EDIT_MAINTAINERS):
|
||||
flash("You do not have permission to edit maintainers", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
form = PackageMaintainersForm(formdata=request.form)
|
||||
if request.method == "GET":
|
||||
form.maintainers_str.data = ", ".join([ x.username for x in package.maintainers if x != package.author ])
|
||||
|
||||
if form.validate_on_submit():
|
||||
usernames = [x.strip().lower() for x in form.maintainers_str.data.split(",")]
|
||||
users = User.query.filter(func.lower(User.username).in_(usernames)).all()
|
||||
|
||||
for user in users:
|
||||
if not user in package.maintainers:
|
||||
addNotification(user, current_user, NotificationType.MAINTAINER,
|
||||
"Added you as a maintainer of {}".format(package.title), package.getDetailsURL(), package)
|
||||
|
||||
for user in package.maintainers:
|
||||
if user != package.author and not user in users:
|
||||
addNotification(user, current_user, NotificationType.MAINTAINER,
|
||||
"Removed you as a maintainer of {}".format(package.title), package.getDetailsURL(), package)
|
||||
|
||||
package.maintainers.clear()
|
||||
package.maintainers.extend(users)
|
||||
if package.author not in package.maintainers:
|
||||
package.maintainers.append(package.author)
|
||||
|
||||
msg = "Edited {} maintainers".format(package.title)
|
||||
addNotification(package.author, current_user, NotificationType.MAINTAINER, msg, package.getDetailsURL(), package)
|
||||
severity = AuditSeverity.NORMAL if current_user == package.author else AuditSeverity.MODERATION
|
||||
addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all()
|
||||
|
||||
return render_template("packages/edit_maintainers.html",
|
||||
package=package, form=form, users=users)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def remove_self_maintainers(package):
|
||||
if not current_user in package.maintainers:
|
||||
flash("You are not a maintainer", "danger")
|
||||
|
||||
elif current_user == package.author:
|
||||
flash("Package owners cannot remove themselves as maintainers", "danger")
|
||||
|
||||
else:
|
||||
package.maintainers.remove(current_user)
|
||||
|
||||
addNotification(package.author, current_user, NotificationType.MAINTAINER,
|
||||
"Removed themself as a maintainer of {}".format(package.title), package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/import-meta/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def update_from_release(package):
|
||||
if not package.checkPerm(current_user, Permission.REIMPORT_META):
|
||||
flash("You don't have permission to reimport meta", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
release = package.releases.first()
|
||||
if not release:
|
||||
flash("Release needed", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
msg = "Updated meta from latest release"
|
||||
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT,
|
||||
msg, package.getDetailsURL(), package)
|
||||
severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR
|
||||
addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
task_id = uuid()
|
||||
zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
|
||||
checkZipRelease.apply_async((release.id, zippath), task_id=task_id)
|
||||
|
||||
return redirect(url_for("tasks.check", id=task_id, r=package.getEditURL()))
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
|
||||
from . import bp
|
||||
|
||||
from app.rediscache import has_key, set_key, make_download_key
|
||||
from app.models import *
|
||||
from app.tasks.importtasks import makeVCSRelease
|
||||
from app.utils import *
|
||||
|
||||
from celery import uuid
|
||||
from flask_login import login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.logic.releases import do_create_vcs_release, LogicError, do_create_zip_release
|
||||
from app.rediscache import has_key, set_key, make_download_key
|
||||
from app.tasks.importtasks import check_update_config
|
||||
from app.utils import *
|
||||
from . import bp
|
||||
|
||||
|
||||
def get_mt_releases(is_max):
|
||||
@@ -45,7 +42,7 @@ def get_mt_releases(is_max):
|
||||
class CreatePackageReleaseForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
||||
uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
|
||||
vcsLabel = StringField("VCS Commit Hash, Branch, or Tag", default="master")
|
||||
vcsLabel = StringField("Git reference (ie: commit hash, branch, or tag)", default=None)
|
||||
fileUpload = FileField("File Upload")
|
||||
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
|
||||
query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name)
|
||||
@@ -55,7 +52,7 @@ class CreatePackageReleaseForm(FlaskForm):
|
||||
|
||||
class EditPackageReleaseForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(1, 30)])
|
||||
url = StringField("URL", [URL])
|
||||
url = StringField("URL", [Optional()])
|
||||
task_id = StringField("Task ID", filters = [lambda x: x or None])
|
||||
approved = BooleanField("Is Approved")
|
||||
min_rel = QuerySelectField("Minimum Minetest Version", [InputRequired()],
|
||||
@@ -74,49 +71,29 @@ def create_release(package):
|
||||
# Initial form class from post data and default data
|
||||
form = CreatePackageReleaseForm()
|
||||
if package.repo is not None:
|
||||
form["uploadOpt"].choices = [("vcs", "From Git Commit or Branch"), ("upload", "File Upload")]
|
||||
if request.method != "POST":
|
||||
form["uploadOpt"].choices = [("vcs", "Import from Git"), ("upload", "Upload .zip file")]
|
||||
if request.method == "GET":
|
||||
form["uploadOpt"].data = "vcs"
|
||||
form.vcsLabel.data = request.args.get("ref")
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
if form["uploadOpt"].data == "vcs":
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = form["title"].data
|
||||
rel.url = ""
|
||||
rel.task_id = uuid()
|
||||
rel.min_rel = form["min_rel"].data.getActual()
|
||||
rel.max_rel = form["max_rel"].data.getActual()
|
||||
db.session.add(rel)
|
||||
db.session.commit()
|
||||
|
||||
makeVCSRelease.apply_async((rel.id, form["vcsLabel"].data), task_id=rel.task_id)
|
||||
|
||||
msg = "{}: Release {} created".format(package.title, rel.title)
|
||||
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||
db.session.commit()
|
||||
if request.method == "GET":
|
||||
form.title.data = request.args.get("title")
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
if form["uploadOpt"].data == "vcs":
|
||||
rel = do_create_vcs_release(current_user, package, form.title.data,
|
||||
form.vcsLabel.data, form.min_rel.data.getActual(), form.max_rel.data.getActual())
|
||||
else:
|
||||
rel = do_create_zip_release(current_user, package, form.title.data,
|
||||
form.fileUpload.data, form.min_rel.data.getActual(), form.max_rel.data.getActual())
|
||||
return redirect(url_for("tasks.check", id=rel.task_id, r=rel.getEditURL()))
|
||||
else:
|
||||
uploadedPath = doFileUpload(form.fileUpload.data, "zip", "a zip file")
|
||||
if uploadedPath is not None:
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = form["title"].data
|
||||
rel.url = uploadedPath
|
||||
rel.min_rel = form["min_rel"].data.getActual()
|
||||
rel.max_rel = form["max_rel"].data.getActual()
|
||||
rel.approve(current_user)
|
||||
db.session.add(rel)
|
||||
db.session.commit()
|
||||
|
||||
msg = "{}: Release {} created".format(package.title, rel.title)
|
||||
triggerNotif(package.author, current_user, msg, rel.getEditURL())
|
||||
db.session.commit()
|
||||
return redirect(package.getDetailsURL())
|
||||
except LogicError as e:
|
||||
flash(e.message, "danger")
|
||||
|
||||
return render_template("packages/release_new.html", package=package, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/releases/<id>/download/")
|
||||
@is_package_page
|
||||
def download_release(package, id):
|
||||
@@ -125,20 +102,20 @@ def download_release(package, id):
|
||||
abort(404)
|
||||
|
||||
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
|
||||
if ip is not None:
|
||||
if ip is not None and not is_user_bot():
|
||||
key = make_download_key(ip, release.package)
|
||||
if not has_key(key):
|
||||
set_key(key, "true")
|
||||
|
||||
bonus = 1
|
||||
if not package.getIsFOSS():
|
||||
bonus *= 0.1
|
||||
|
||||
PackageRelease.query.filter_by(id=release.id).update({
|
||||
"downloads": PackageRelease.downloads + 1
|
||||
})
|
||||
|
||||
Package.query.filter_by(id=package.id).update({
|
||||
"downloads": Package.downloads + 1,
|
||||
"score_downloads": Package.score_downloads + bonus,
|
||||
"score": Package.score + bonus
|
||||
})
|
||||
|
||||
@@ -146,16 +123,15 @@ def download_release(package, id):
|
||||
|
||||
return redirect(release.url, code=300)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/releases/<id>/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def edit_release(package, id):
|
||||
release = PackageRelease.query.get(id)
|
||||
release : PackageRelease = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
abort(404)
|
||||
|
||||
clearNotifications(release.getEditURL())
|
||||
|
||||
canEdit = package.checkPerm(current_user, Permission.MAKE_RELEASE)
|
||||
canApprove = package.checkPerm(current_user, Permission.APPROVE_RELEASE)
|
||||
if not (canEdit or canApprove):
|
||||
@@ -163,8 +139,12 @@ def edit_release(package, id):
|
||||
|
||||
# Initial form class from post data and default data
|
||||
form = EditPackageReleaseForm(formdata=request.form, obj=release)
|
||||
if request.method == "POST" and form.validate():
|
||||
wasApproved = release.approved
|
||||
|
||||
if request.method == "GET":
|
||||
# HACK: fix bug in wtforms
|
||||
form.approved.data = release.approved
|
||||
|
||||
if form.validate_on_submit():
|
||||
if canEdit:
|
||||
release.title = form["title"].data
|
||||
release.min_rel = form["min_rel"].data.getActual()
|
||||
@@ -176,10 +156,10 @@ def edit_release(package, id):
|
||||
if release.task_id is not None:
|
||||
release.task_id = None
|
||||
|
||||
if canApprove:
|
||||
release.approved = form["approved"].data
|
||||
else:
|
||||
release.approved = wasApproved
|
||||
if form.approved.data:
|
||||
release.approve(current_user)
|
||||
elif canApprove:
|
||||
release.approved = False
|
||||
|
||||
db.session.commit()
|
||||
return redirect(package.getDetailsURL())
|
||||
@@ -211,7 +191,7 @@ def bulk_change_release(package):
|
||||
|
||||
if request.method == "GET":
|
||||
form.only_change_none.data = True
|
||||
elif request.method == "POST" and form.validate():
|
||||
elif form.validate_on_submit():
|
||||
only_change_none = form.only_change_none.data
|
||||
|
||||
for release in package.releases.all():
|
||||
@@ -225,3 +205,136 @@ def bulk_change_release(package):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
return render_template("packages/release_bulk_change.html", package=package, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/releases/<id>/delete/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def delete_release(package, id):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None or release.package != package:
|
||||
abort(404)
|
||||
|
||||
if not release.checkPerm(current_user, Permission.DELETE_RELEASE):
|
||||
return redirect(release.getEditURL())
|
||||
|
||||
db.session.delete(release)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
|
||||
class PackageUpdateConfigFrom(FlaskForm):
|
||||
trigger = RadioField("Trigger", [InputRequired()], choices=PackageUpdateTrigger.choices(), coerce=PackageUpdateTrigger.coerce,
|
||||
default=PackageUpdateTrigger.TAG)
|
||||
ref = StringField("Branch name", [Optional()], default=None)
|
||||
action = RadioField("Action", [InputRequired()], choices=[("notification", "Send notification and mark as outdated"), ("make_release", "Create release")], default="make_release")
|
||||
submit = SubmitField("Save Settings")
|
||||
disable = SubmitField("Disable Automation")
|
||||
|
||||
|
||||
def set_update_config(package, form):
|
||||
if package.update_config is None:
|
||||
package.update_config = PackageUpdateConfig()
|
||||
db.session.add(package.update_config)
|
||||
|
||||
form.populate_obj(package.update_config)
|
||||
package.update_config.ref = nonEmptyOrNone(form.ref.data)
|
||||
package.update_config.make_release = form.action.data == "make_release"
|
||||
|
||||
if package.update_config.trigger == PackageUpdateTrigger.COMMIT:
|
||||
if package.update_config.last_commit is None:
|
||||
last_release = package.releases.first()
|
||||
if last_release and last_release.commit_hash:
|
||||
package.update_config.last_commit = last_release.commit_hash
|
||||
elif package.update_config.trigger == PackageUpdateTrigger.TAG:
|
||||
# Only create releases for tags created after this
|
||||
package.update_config.last_commit = None
|
||||
package.update_config.last_tag = None
|
||||
|
||||
package.update_config.outdated_at = None
|
||||
package.update_config.auto_created = False
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if package.update_config.last_commit is None:
|
||||
check_update_config.delay(package.id)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/update-config/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def update_config(package):
|
||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||
abort(403)
|
||||
|
||||
if not package.repo:
|
||||
flash("Please add a Git repository URL in order to set up automatic releases", "danger")
|
||||
return redirect(package.getEditURL())
|
||||
|
||||
form = PackageUpdateConfigFrom(obj=package.update_config)
|
||||
if request.method == "GET":
|
||||
if package.update_config:
|
||||
form.action.data = "make_release" if package.update_config.make_release else "notification"
|
||||
elif request.args.get("action") == "notification":
|
||||
form.trigger.data = PackageUpdateTrigger.COMMIT
|
||||
form.action.data = "notification"
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.disable.data:
|
||||
flash("Deleted update configuration", "success")
|
||||
if package.update_config:
|
||||
db.session.delete(package.update_config)
|
||||
db.session.commit()
|
||||
else:
|
||||
set_update_config(package, form)
|
||||
|
||||
if not form.disable.data and package.releases.count() == 0:
|
||||
flash("Now, please create an initial release", "success")
|
||||
return redirect(package.getCreateReleaseURL())
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
return render_template("packages/update_config.html", package=package, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/setup-releases/")
|
||||
@login_required
|
||||
@is_package_page
|
||||
def setup_releases(package):
|
||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||
abort(403)
|
||||
|
||||
if package.update_config:
|
||||
return redirect(package.getUpdateConfigURL())
|
||||
|
||||
return render_template("packages/release_wizard.html", package=package)
|
||||
|
||||
|
||||
@bp.route("/user/update-configs/")
|
||||
@bp.route("/users/<username>/update-configs/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def bulk_update_config(username=None):
|
||||
if username is None:
|
||||
return redirect(url_for("packages.bulk_update_config", username=current_user.username))
|
||||
|
||||
user: User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR):
|
||||
abort(403)
|
||||
|
||||
form = PackageUpdateConfigFrom()
|
||||
if form.validate_on_submit():
|
||||
for package in user.packages.filter(Package.state != PackageState.DELETED, Package.repo.isnot(None)).all():
|
||||
set_update_config(package, form)
|
||||
|
||||
return redirect(url_for("packages.bulk_update_config", username=username))
|
||||
|
||||
confs = user.packages \
|
||||
.filter(Package.state != PackageState.DELETED,
|
||||
Package.update_config.has()) \
|
||||
.order_by(db.asc(Package.title)).all()
|
||||
|
||||
return render_template("packages/bulk_update_conf.html", user=user, confs=confs, form=form)
|
||||
|
||||
143
app/blueprints/packages/reviews.py
Normal file
143
app/blueprints/packages/reviews.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from . import bp
|
||||
|
||||
from flask import *
|
||||
from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType
|
||||
from app.utils import is_package_page, addNotification, get_int_or_abort
|
||||
|
||||
|
||||
@bp.route("/reviews/")
|
||||
def list_reviews():
|
||||
page = get_int_or_abort(request.args.get("page"), 1)
|
||||
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||
|
||||
pagination = PackageReview.query.order_by(db.desc(PackageReview.created_at)).paginate(page, num, True)
|
||||
return render_template("packages/reviews_list.html", pagination=pagination, reviews=pagination.items)
|
||||
|
||||
|
||||
class ReviewForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||
comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
|
||||
recommends = RadioField("Private", [InputRequired()], choices=[("yes", "Yes"), ("no", "No")])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@bp.route("/packages/<author>/<name>/review/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def review(package):
|
||||
if current_user in package.maintainers:
|
||||
flash("You can't review your own package!", "danger")
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
review = PackageReview.query.filter_by(package=package, author=current_user).first()
|
||||
|
||||
form = ReviewForm(formdata=request.form, obj=review)
|
||||
|
||||
# Set default values
|
||||
if request.method == "GET" and review:
|
||||
form.title.data = review.thread.title
|
||||
form.recommends.data = "yes" if review.recommends else "no"
|
||||
form.comment.data = review.thread.replies[0].comment
|
||||
|
||||
# Validate and submit
|
||||
elif form.validate_on_submit():
|
||||
was_new = False
|
||||
if not review:
|
||||
was_new = True
|
||||
review = PackageReview()
|
||||
review.package = package
|
||||
review.author = current_user
|
||||
db.session.add(review)
|
||||
|
||||
review.recommends = form.recommends.data == "yes"
|
||||
|
||||
thread = review.thread
|
||||
if not thread:
|
||||
thread = Thread()
|
||||
thread.author = current_user
|
||||
thread.private = False
|
||||
thread.package = package
|
||||
thread.review = review
|
||||
db.session.add(thread)
|
||||
|
||||
thread.watchers.append(current_user)
|
||||
|
||||
reply = ThreadReply()
|
||||
reply.thread = thread
|
||||
reply.author = current_user
|
||||
reply.comment = form.comment.data
|
||||
db.session.add(reply)
|
||||
|
||||
thread.replies.append(reply)
|
||||
else:
|
||||
reply = thread.replies[0]
|
||||
reply.comment = form.comment.data
|
||||
|
||||
thread.title = form.title.data
|
||||
|
||||
db.session.commit()
|
||||
|
||||
package.recalcScore()
|
||||
|
||||
if was_new:
|
||||
notif_msg = "New review '{}'".format(form.title.data)
|
||||
type = NotificationType.NEW_REVIEW
|
||||
else:
|
||||
notif_msg = "Updated review '{}'".format(form.title.data)
|
||||
type = NotificationType.OTHER
|
||||
|
||||
addNotification(package.maintainers, current_user, type, notif_msg,
|
||||
url_for("threads.view", id=thread.id), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
return render_template("packages/review_create_edit.html",
|
||||
form=form, package=package, review=review)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/review/delete/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def delete_review(package):
|
||||
review = PackageReview.query.filter_by(package=package, author=current_user).first()
|
||||
if review is None or review.package != package:
|
||||
abort(404)
|
||||
|
||||
thread = review.thread
|
||||
|
||||
reply = ThreadReply()
|
||||
reply.thread = thread
|
||||
reply.author = current_user
|
||||
reply.comment = "_converted review into a thread_"
|
||||
db.session.add(reply)
|
||||
|
||||
thread.review = None
|
||||
|
||||
notif_msg = "Deleted review '{}', comments were kept as a thread".format(thread.title)
|
||||
addNotification(package.maintainers, current_user, NotificationType.OTHER, notif_msg, url_for("threads.view", id=thread.id), package)
|
||||
|
||||
db.session.delete(review)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(thread.getViewURL())
|
||||
@@ -1,73 +1,98 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
|
||||
from . import bp
|
||||
|
||||
from app.models import *
|
||||
from app.utils import *
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_login import login_required
|
||||
from wtforms import *
|
||||
from wtforms.ext.sqlalchemy.fields import QuerySelectField
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.utils import *
|
||||
from . import bp
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
|
||||
|
||||
|
||||
class CreateScreenshotForm(FlaskForm):
|
||||
title = StringField("Title/Caption", [Optional()])
|
||||
title = StringField("Title/Caption", [Optional(), Length(-1, 100)])
|
||||
fileUpload = FileField("File Upload", [InputRequired()])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
class EditScreenshotForm(FlaskForm):
|
||||
title = StringField("Title/Caption", [Optional()])
|
||||
title = StringField("Title/Caption", [Optional(), Length(-1, 100)])
|
||||
approved = BooleanField("Is Approved")
|
||||
delete = BooleanField("Delete")
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
class EditPackageScreenshotsForm(FlaskForm):
|
||||
cover_image = QuerySelectField("Cover Image", [DataRequired()], allow_blank=True, get_pk=lambda a: a.id, get_label=lambda a: a.title)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/screenshots/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def screenshots(package):
|
||||
if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
if package.screenshots.count() == 0:
|
||||
return redirect(package.getNewScreenshotURL())
|
||||
|
||||
form = EditPackageScreenshotsForm(obj=package)
|
||||
form.cover_image.query = package.screenshots
|
||||
|
||||
if request.method == "POST":
|
||||
order = request.form.get("order")
|
||||
if order:
|
||||
try:
|
||||
do_order_screenshots(current_user, package, order.split(","))
|
||||
return redirect(package.getDetailsURL())
|
||||
except LogicError as e:
|
||||
flash(e.message, "danger")
|
||||
|
||||
if form.validate_on_submit():
|
||||
form.populate_obj(package)
|
||||
db.session.commit()
|
||||
|
||||
return render_template("packages/screenshots.html", package=package, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/screenshots/new/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def create_screenshot(package, id=None):
|
||||
def create_screenshot(package):
|
||||
if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
# Initial form class from post data and default data
|
||||
form = CreateScreenshotForm()
|
||||
if request.method == "POST" and form.validate():
|
||||
uploadedPath = doFileUpload(form.fileUpload.data, "image",
|
||||
"a PNG or JPG image file")
|
||||
if uploadedPath is not None:
|
||||
ss = PackageScreenshot()
|
||||
ss.package = package
|
||||
ss.title = form["title"].data or "Untitled"
|
||||
ss.url = uploadedPath
|
||||
ss.approved = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
|
||||
db.session.add(ss)
|
||||
|
||||
msg = "{}: Screenshot added {}" \
|
||||
.format(package.title, ss.title)
|
||||
triggerNotif(package.author, current_user, msg, package.getDetailsURL())
|
||||
db.session.commit()
|
||||
return redirect(package.getDetailsURL())
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
do_create_screenshot(current_user, package, form.title.data, form.fileUpload.data)
|
||||
return redirect(package.getEditScreenshotsURL())
|
||||
except LogicError as e:
|
||||
flash(e.message, "danger")
|
||||
|
||||
return render_template("packages/screenshot_new.html", package=package, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/screenshots/<id>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
@@ -79,28 +104,44 @@ def edit_screenshot(package, id):
|
||||
canEdit = package.checkPerm(current_user, Permission.ADD_SCREENSHOTS)
|
||||
canApprove = package.checkPerm(current_user, Permission.APPROVE_SCREENSHOT)
|
||||
if not (canEdit or canApprove):
|
||||
return redirect(package.getDetailsURL())
|
||||
|
||||
clearNotifications(screenshot.getEditURL())
|
||||
return redirect(package.getEditScreenshotsURL())
|
||||
|
||||
# Initial form class from post data and default data
|
||||
form = EditScreenshotForm(formdata=request.form, obj=screenshot)
|
||||
if request.method == "POST" and form.validate():
|
||||
if canEdit and form["delete"].data:
|
||||
PackageScreenshot.query.filter_by(id=id).delete()
|
||||
form = EditScreenshotForm(obj=screenshot)
|
||||
if form.validate_on_submit():
|
||||
wasApproved = screenshot.approved
|
||||
|
||||
if canEdit:
|
||||
screenshot.title = form["title"].data or "Untitled"
|
||||
|
||||
if canApprove:
|
||||
screenshot.approved = form["approved"].data
|
||||
else:
|
||||
wasApproved = screenshot.approved
|
||||
|
||||
if canEdit:
|
||||
screenshot.title = form["title"].data or "Untitled"
|
||||
|
||||
if canApprove:
|
||||
screenshot.approved = form["approved"].data
|
||||
else:
|
||||
screenshot.approved = wasApproved
|
||||
screenshot.approved = wasApproved
|
||||
|
||||
db.session.commit()
|
||||
return redirect(package.getDetailsURL())
|
||||
return redirect(package.getEditScreenshotsURL())
|
||||
|
||||
return render_template("packages/screenshot_edit.html", package=package, screenshot=screenshot, form=form)
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/screenshots/<id>/delete/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def delete_screenshot(package, id):
|
||||
screenshot = PackageScreenshot.query.get(id)
|
||||
if screenshot is None or screenshot.package != package:
|
||||
abort(404)
|
||||
|
||||
if not package.checkPerm(current_user, Permission.ADD_SCREENSHOTS):
|
||||
flash("Permission denied", "danger")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
if package.cover_image == screenshot:
|
||||
package.cover_image = None
|
||||
db.session.merge(package)
|
||||
|
||||
db.session.delete(screenshot)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(package.getEditScreenshotsURL())
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
import flask_menu as menu
|
||||
from flask_login import login_required
|
||||
|
||||
from app import csrf
|
||||
from app.models import *
|
||||
from app.tasks import celery, TaskError
|
||||
from app.tasks import celery
|
||||
from app.tasks.importtasks import getMeta
|
||||
from app.utils import shouldReturnJson
|
||||
from app.utils import *
|
||||
|
||||
bp = Blueprint("tasks", __name__)
|
||||
@@ -45,7 +43,7 @@ def check(id):
|
||||
traceback = result.traceback
|
||||
result = result.result
|
||||
|
||||
info = None
|
||||
None
|
||||
if isinstance(result, Exception):
|
||||
info = {
|
||||
'id': id,
|
||||
|
||||
@@ -1,40 +1,53 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
|
||||
bp = Blueprint("threads", __name__)
|
||||
|
||||
from flask_user import *
|
||||
from flask_login import current_user, login_required
|
||||
from app import menu
|
||||
from app.models import *
|
||||
from app.utils import triggerNotif, clearNotifications
|
||||
|
||||
import datetime
|
||||
|
||||
from app.utils import addNotification, isYes, addAuditLog
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.utils import get_int_or_abort
|
||||
|
||||
@menu.register_menu(bp, ".threads", "Threads", order=20)
|
||||
@bp.route("/threads/")
|
||||
def list_all():
|
||||
query = Thread.query
|
||||
if not Permission.SEE_THREAD.check(current_user):
|
||||
query = query.filter_by(private=False)
|
||||
return render_template("threads/list.html", threads=query.all())
|
||||
|
||||
pid = request.args.get("pid")
|
||||
if pid:
|
||||
pid = get_int_or_abort(pid)
|
||||
query = query.filter_by(package_id=pid)
|
||||
|
||||
query = query.filter_by(review_id=None)
|
||||
|
||||
query = query.order_by(db.desc(Thread.created_at))
|
||||
|
||||
page = get_int_or_abort(request.args.get("page"), 1)
|
||||
num = min(40, get_int_or_abort(request.args.get("n"), 100))
|
||||
|
||||
pagination = query.paginate(page, num, True)
|
||||
|
||||
return render_template("threads/list.html", pagination=pagination, threads=pagination.items)
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/subscribe/", methods=["POST"])
|
||||
@@ -51,7 +64,7 @@ def subscribe(id):
|
||||
thread.watchers.append(current_user)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("threads.view", id=id))
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/unsubscribe/", methods=["POST"])
|
||||
@@ -66,15 +79,138 @@ def unsubscribe(id):
|
||||
thread.watchers.remove(current_user)
|
||||
db.session.commit()
|
||||
else:
|
||||
flash("Not subscribed to thread", "success")
|
||||
flash("Already not subscribed!", "success")
|
||||
|
||||
return redirect(url_for("threads.view", id=id))
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/set-lock/", methods=["POST"])
|
||||
@login_required
|
||||
def set_lock(id):
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.LOCK_THREAD):
|
||||
abort(404)
|
||||
|
||||
thread.locked = isYes(request.args.get("lock"))
|
||||
if thread.locked is None:
|
||||
abort(400)
|
||||
|
||||
msg = None
|
||||
if thread.locked:
|
||||
msg = "Locked thread '{}'".format(thread.title)
|
||||
flash("Locked thread", "success")
|
||||
else:
|
||||
msg = "Unlocked thread '{}'".format(thread.title)
|
||||
flash("Unlocked thread", "success")
|
||||
|
||||
addNotification(thread.watchers, current_user, NotificationType.OTHER, msg, thread.getViewURL(), thread.package)
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, NotificationType.OTHER, msg, thread.getViewURL(), thread.package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/delete/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_thread(id):
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.DELETE_THREAD):
|
||||
abort(404)
|
||||
|
||||
if request.method == "GET":
|
||||
return render_template("threads/delete_thread.html", thread=thread)
|
||||
|
||||
summary = "\n\n".join([("<{}> {}".format(reply.author.display_name, reply.comment)) for reply in thread.replies])
|
||||
|
||||
msg = "Deleted thread {} by {}".format(thread.title, thread.author.display_name)
|
||||
|
||||
db.session.delete(thread)
|
||||
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None, thread.package, summary)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/delete-reply/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def delete_reply(id):
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None:
|
||||
abort(404)
|
||||
|
||||
reply_id = request.args.get("reply")
|
||||
if reply_id is None:
|
||||
abort(404)
|
||||
|
||||
reply = ThreadReply.query.get(reply_id)
|
||||
if reply is None or reply.thread != thread:
|
||||
abort(404)
|
||||
|
||||
if thread.replies[0] == reply:
|
||||
flash("Cannot delete thread opening post!", "danger")
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
if not reply.checkPerm(current_user, Permission.DELETE_REPLY):
|
||||
abort(403)
|
||||
|
||||
if request.method == "GET":
|
||||
return render_template("threads/delete_reply.html", thread=thread, reply=reply)
|
||||
|
||||
msg = "Deleted reply by {}".format(reply.author.display_name)
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
|
||||
|
||||
db.session.delete(reply)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
|
||||
class CommentForm(FlaskForm):
|
||||
comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
|
||||
submit = SubmitField("Comment")
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/edit/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_reply(id):
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None:
|
||||
abort(404)
|
||||
|
||||
reply_id = request.args.get("reply")
|
||||
if reply_id is None:
|
||||
abort(404)
|
||||
|
||||
reply = ThreadReply.query.get(reply_id)
|
||||
if reply is None or reply.thread != thread:
|
||||
abort(404)
|
||||
|
||||
if not reply.checkPerm(current_user, Permission.EDIT_REPLY):
|
||||
abort(403)
|
||||
|
||||
form = CommentForm(formdata=request.form, obj=reply)
|
||||
if form.validate_on_submit():
|
||||
comment = form.comment.data
|
||||
|
||||
msg = "Edited reply by {}".format(reply.author.display_name)
|
||||
severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION
|
||||
addNotification(reply.author, current_user, NotificationType.OTHER, msg, thread.getViewURL(), thread.package)
|
||||
addAuditLog(severity, current_user, msg, thread.getViewURL(), thread.package, reply.comment)
|
||||
|
||||
reply.comment = comment
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
return render_template("threads/edit_reply.html", thread=thread, reply=reply, form=form)
|
||||
|
||||
|
||||
@bp.route("/threads/<int:id>/", methods=["GET", "POST"])
|
||||
def view(id):
|
||||
clearNotifications(url_for("threads.view", id=id))
|
||||
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
abort(404)
|
||||
@@ -82,14 +218,15 @@ def view(id):
|
||||
if current_user.is_authenticated and request.method == "POST":
|
||||
comment = request.form["comment"]
|
||||
|
||||
if not thread.checkPerm(current_user, Permission.COMMENT_THREAD):
|
||||
flash("You cannot comment on this thread", "danger")
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
if not current_user.canCommentRL():
|
||||
flash("Please wait before commenting again", "danger")
|
||||
if package:
|
||||
return redirect(package.getDetailsURL())
|
||||
else:
|
||||
return redirect(url_for("homepage.home"))
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
if len(comment) <= 500 and len(comment) > 3:
|
||||
if 2000 >= len(comment) > 3:
|
||||
reply = ThreadReply()
|
||||
reply.author = current_user
|
||||
reply.comment = comment
|
||||
@@ -99,33 +236,25 @@ def view(id):
|
||||
if not current_user in thread.watchers:
|
||||
thread.watchers.append(current_user)
|
||||
|
||||
msg = None
|
||||
if thread.package is None:
|
||||
msg = "New comment on '{}'".format(thread.title)
|
||||
else:
|
||||
msg = "New comment on '{}' on package {}".format(thread.title, thread.package.title)
|
||||
|
||||
|
||||
for user in thread.watchers:
|
||||
if user != current_user:
|
||||
triggerNotif(user, current_user, msg, url_for("threads.view", id=thread.id))
|
||||
|
||||
msg = "New comment on '{}'".format(thread.title)
|
||||
addNotification(thread.watchers, current_user, NotificationType.THREAD_REPLY, msg, thread.getViewURL(), thread.package)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("threads.view", id=id))
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
else:
|
||||
flash("Comment needs to be between 3 and 500 characters.")
|
||||
flash("Comment needs to be between 3 and 2000 characters.")
|
||||
|
||||
return render_template("threads/view.html", thread=thread)
|
||||
|
||||
|
||||
class ThreadForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||
comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
|
||||
comment = TextAreaField("Comment", [InputRequired(), Length(10, 2000)])
|
||||
private = BooleanField("Private")
|
||||
submit = SubmitField("Open Thread")
|
||||
|
||||
|
||||
@bp.route("/threads/new/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new():
|
||||
@@ -135,7 +264,7 @@ def new():
|
||||
if "pid" in request.args:
|
||||
package = Package.query.get(int(request.args.get("pid")))
|
||||
if package is None:
|
||||
flash("Unable to find that package!", "error")
|
||||
flash("Unable to find that package!", "danger")
|
||||
|
||||
# Don't allow making orphan threads on approved packages for now
|
||||
if package is None:
|
||||
@@ -149,13 +278,13 @@ def new():
|
||||
|
||||
# Check that user can make the thread
|
||||
if not package.checkPerm(current_user, Permission.CREATE_THREAD):
|
||||
flash("Unable to create thread!", "error")
|
||||
flash("Unable to create thread!", "danger")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
# Only allow creating one thread when not approved
|
||||
elif is_review_thread and package.review_thread is not None:
|
||||
flash("A review thread already exists!", "error")
|
||||
return redirect(url_for("threads.view", id=package.review_thread.id))
|
||||
flash("A review thread already exists!", "danger")
|
||||
return redirect(package.review_thread.getViewURL())
|
||||
|
||||
elif not current_user.canOpenThreadRL():
|
||||
flash("Please wait before opening another thread", "danger")
|
||||
@@ -171,7 +300,7 @@ def new():
|
||||
form.title.data = request.args.get("title") or ""
|
||||
|
||||
# Validate and submit
|
||||
elif request.method == "POST" and form.validate():
|
||||
elif form.validate_on_submit():
|
||||
thread = Thread()
|
||||
thread.author = current_user
|
||||
thread.title = form.title.data
|
||||
@@ -196,19 +325,20 @@ def new():
|
||||
if is_review_thread:
|
||||
package.review_thread = thread
|
||||
|
||||
notif_msg = None
|
||||
if package is not None:
|
||||
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
|
||||
triggerNotif(package.author, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
||||
else:
|
||||
notif_msg = "New thread '{}'".format(thread.title)
|
||||
if package.state == PackageState.READY_FOR_REVIEW and current_user not in package.maintainers:
|
||||
package.state = PackageState.CHANGES_NEEDED
|
||||
|
||||
for user in User.query.filter(User.rank >= UserRank.EDITOR).all():
|
||||
triggerNotif(user, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
||||
|
||||
notif_msg = "New thread '{}'".format(thread.title)
|
||||
if package is not None:
|
||||
addNotification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.getViewURL(), package)
|
||||
|
||||
editors = User.query.filter(User.rank >= UserRank.EDITOR).all()
|
||||
addNotification(editors, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.getViewURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("threads.view", id=thread.id))
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
|
||||
return render_template("threads/new.html", form=form, allow_private_change=allow_change, package=package)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask import abort, send_file, Blueprint, current_app
|
||||
|
||||
bp = Blueprint("thumbnails", __name__)
|
||||
|
||||
@@ -25,13 +25,19 @@ from PIL import Image
|
||||
ALLOWED_RESOLUTIONS=[(100,67), (270,180), (350,233)]
|
||||
|
||||
def mkdir(path):
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
assert path != "" and path is not None
|
||||
try:
|
||||
if not os.path.isdir(path):
|
||||
os.mkdir(path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
mkdir("app/public/thumbnails/")
|
||||
|
||||
def resize_and_crop(img_path, modified_path, size):
|
||||
img = Image.open(img_path)
|
||||
try:
|
||||
img = Image.open(img_path)
|
||||
except FileNotFoundError:
|
||||
abort(404)
|
||||
|
||||
# Get current and desired ratio for the images
|
||||
img_ratio = img.size[0] / float(img.size[1])
|
||||
@@ -65,10 +71,15 @@ def make_thumbnail(img, level):
|
||||
|
||||
w, h = ALLOWED_RESOLUTIONS[level - 1]
|
||||
|
||||
mkdir("app/public/thumbnails/{:d}/".format(level))
|
||||
upload_dir = current_app.config["UPLOAD_DIR"]
|
||||
thumbnail_dir = current_app.config["THUMBNAIL_DIR"]
|
||||
mkdir(thumbnail_dir)
|
||||
|
||||
cache_filepath = "public/thumbnails/{:d}/{}".format(level, img)
|
||||
source_filepath = "public/uploads/" + img
|
||||
output_dir = os.path.join(thumbnail_dir, str(level))
|
||||
mkdir(output_dir)
|
||||
|
||||
resize_and_crop("app/" + source_filepath, "app/" + cache_filepath, (w, h))
|
||||
cache_filepath = os.path.join(output_dir, img)
|
||||
source_filepath = os.path.join(upload_dir, img)
|
||||
|
||||
resize_and_crop(source_filepath, cache_filepath, (w, h))
|
||||
return send_file(cache_filepath)
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from celery import uuid
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
import flask_menu as menu
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app.models import *
|
||||
from app.querybuilder import QueryBuilder
|
||||
from app.utils import get_int_or_abort
|
||||
from app.utils import get_int_or_abort, addNotification, addAuditLog, isYes
|
||||
from app.tasks.importtasks import makeVCSRelease
|
||||
|
||||
bp = Blueprint("todo", __name__)
|
||||
|
||||
@bp.route("/todo/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def view():
|
||||
def view_editor():
|
||||
canApproveNew = Permission.APPROVE_NEW.check(current_user)
|
||||
canApproveRel = Permission.APPROVE_RELEASE.check(current_user)
|
||||
canApproveScn = Permission.APPROVE_SCREENSHOT.check(current_user)
|
||||
|
||||
packages = None
|
||||
wip_packages = None
|
||||
if canApproveNew:
|
||||
packages = Package.query.filter_by(approved=False, soft_deleted=False).order_by(db.desc(Package.created_at)).all()
|
||||
packages = Package.query.filter_by(state=PackageState.READY_FOR_REVIEW) \
|
||||
.order_by(db.desc(Package.created_at)).all()
|
||||
wip_packages = Package.query.filter(or_(Package.state==PackageState.WIP, Package.state==PackageState.CHANGES_NEEDED)) \
|
||||
.order_by(db.desc(Package.created_at)).all()
|
||||
|
||||
releases = None
|
||||
if canApproveRel:
|
||||
@@ -52,22 +59,23 @@ def view():
|
||||
|
||||
PackageScreenshot.query.update({ "approved": True })
|
||||
db.session.commit()
|
||||
return redirect(url_for("todo.view"))
|
||||
return redirect(url_for("todo.view_editor"))
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
topic_query = ForumTopic.query \
|
||||
.filter_by(discarded=False)
|
||||
total_packages = Package.query.filter_by(state=PackageState.APPROVED).count()
|
||||
total_to_tag = Package.query.filter_by(state=PackageState.APPROVED, tags=None).count()
|
||||
|
||||
total_topics = topic_query.count()
|
||||
topics_to_add = topic_query \
|
||||
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||
.count()
|
||||
unfulfilled_meta_packages = MetaPackage.query \
|
||||
.filter(~ MetaPackage.packages.any(state=PackageState.APPROVED)) \
|
||||
.filter(MetaPackage.dependencies.any(optional=False)) \
|
||||
.order_by(db.asc(MetaPackage.name)).count()
|
||||
|
||||
return render_template("todo/list.html", title="Reports and Work Queue",
|
||||
packages=packages, releases=releases, screenshots=screenshots,
|
||||
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
|
||||
topics_to_add=topics_to_add, total_topics=total_topics)
|
||||
return render_template("todo/editor.html", current_tab="editor",
|
||||
packages=packages, wip_packages=wip_packages, releases=releases, screenshots=screenshots,
|
||||
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
|
||||
total_packages=total_packages, total_to_tag=total_to_tag,
|
||||
unfulfilled_meta_packages=unfulfilled_meta_packages)
|
||||
|
||||
|
||||
@bp.route("/todo/topics/")
|
||||
@@ -89,14 +97,151 @@ def topics():
|
||||
num = 100
|
||||
|
||||
query = query.paginate(page, num, True)
|
||||
next_url = url_for("todo.topics", page=query.next_num, query=qb.search, \
|
||||
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||
next_url = url_for("todo.topics", page=query.next_num, query=qb.search,
|
||||
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||
if query.has_next else None
|
||||
prev_url = url_for("todo.topics", page=query.prev_num, query=qb.search, \
|
||||
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||
prev_url = url_for("todo.topics", page=query.prev_num, query=qb.search,
|
||||
show_discarded=qb.show_discarded, n=num, sort=qb.order_by) \
|
||||
if query.has_prev else None
|
||||
|
||||
return render_template("todo/topics.html", topics=query.items, total=total, \
|
||||
topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded, \
|
||||
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, \
|
||||
return render_template("todo/topics.html", current_tab="topics", topics=query.items, total=total,
|
||||
topic_count=topic_count, query=qb.search, show_discarded=qb.show_discarded,
|
||||
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages,
|
||||
n=num, sort_by=qb.order_by)
|
||||
|
||||
|
||||
@bp.route("/todo/tags/")
|
||||
@login_required
|
||||
def tags():
|
||||
qb = QueryBuilder(request.args)
|
||||
qb.setSortIfNone("score", "desc")
|
||||
query = qb.buildPackageQuery()
|
||||
|
||||
tags = Tag.query.order_by(db.asc(Tag.title)).all()
|
||||
|
||||
return render_template("todo/tags.html", current_tab="tags", packages=query.all(), tags=tags)
|
||||
|
||||
|
||||
@bp.route("/user/tags/")
|
||||
def tags_user():
|
||||
return redirect(url_for('todo.tags', author=current_user.username))
|
||||
|
||||
|
||||
@bp.route("/todo/metapackages/")
|
||||
@login_required
|
||||
def metapackages():
|
||||
mpackages = MetaPackage.query \
|
||||
.filter(~ MetaPackage.packages.any(state=PackageState.APPROVED)) \
|
||||
.filter(MetaPackage.dependencies.any(optional=False)) \
|
||||
.order_by(db.asc(MetaPackage.name)).all()
|
||||
|
||||
return render_template("todo/metapackages.html", mpackages=mpackages)
|
||||
|
||||
|
||||
@bp.route("/user/todo/")
|
||||
@bp.route("/users/<username>/todo/")
|
||||
@login_required
|
||||
def view_user(username=None):
|
||||
if username is None:
|
||||
return redirect(url_for("todo.view_user", username=current_user.username))
|
||||
|
||||
user : User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR):
|
||||
abort(403)
|
||||
|
||||
unapproved_packages = user.packages \
|
||||
.filter(or_(Package.state == PackageState.WIP,
|
||||
Package.state == PackageState.CHANGES_NEEDED)) \
|
||||
.order_by(db.asc(Package.created_at)).all()
|
||||
|
||||
outdated_packages = user.maintained_packages \
|
||||
.filter(Package.state != PackageState.DELETED,
|
||||
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \
|
||||
.order_by(db.asc(Package.title)).all()
|
||||
|
||||
topics_to_add = ForumTopic.query \
|
||||
.filter_by(author_id=user.id) \
|
||||
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
|
||||
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||
.all()
|
||||
|
||||
needs_tags = user.maintained_packages \
|
||||
.filter(Package.state != PackageState.DELETED) \
|
||||
.filter_by(tags=None).order_by(db.asc(Package.title)).all()
|
||||
|
||||
return render_template("todo/user.html", current_tab="user", user=user,
|
||||
unapproved_packages=unapproved_packages, outdated_packages=outdated_packages,
|
||||
needs_tags=needs_tags, topics_to_add=topics_to_add)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/update-configs/apply-all/", methods=["POST"])
|
||||
@login_required
|
||||
def apply_all_updates(username):
|
||||
user: User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if current_user != user and not current_user.rank.atLeast(UserRank.EDITOR):
|
||||
abort(403)
|
||||
|
||||
outdated_packages = user.maintained_packages \
|
||||
.filter(Package.state != PackageState.DELETED,
|
||||
Package.update_config.has(PackageUpdateConfig.outdated_at.isnot(None))) \
|
||||
.order_by(db.asc(Package.title)).all()
|
||||
|
||||
for package in outdated_packages:
|
||||
if not package.checkPerm(current_user, Permission.MAKE_RELEASE):
|
||||
continue
|
||||
|
||||
if package.releases.filter(or_(PackageRelease.task_id.isnot(None),
|
||||
PackageRelease.commit_hash==package.update_config.last_commit)).count() > 0:
|
||||
continue
|
||||
|
||||
title = package.update_config.get_title()
|
||||
ref = package.update_config.get_ref()
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = title
|
||||
rel.url = ""
|
||||
rel.task_id = uuid()
|
||||
db.session.add(rel)
|
||||
db.session.commit()
|
||||
|
||||
makeVCSRelease.apply_async((rel.id, ref),
|
||||
task_id=rel.task_id)
|
||||
|
||||
msg = "Created release {} (Applied all Git Update Detection)".format(rel.title)
|
||||
addNotification(package.maintainers, current_user, NotificationType.PACKAGE_EDIT, msg,
|
||||
rel.getEditURL(), package)
|
||||
addAuditLog(AuditSeverity.NORMAL, current_user, msg, package.getDetailsURL(), package)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("todo.view_user", username=username))
|
||||
|
||||
|
||||
@bp.route("/todo/outdated/")
|
||||
@login_required
|
||||
def outdated():
|
||||
is_mtm_only = isYes(request.args.get("mtm"))
|
||||
|
||||
query = db.session.query(Package).select_from(PackageUpdateConfig) \
|
||||
.filter(PackageUpdateConfig.outdated_at.isnot(None)) \
|
||||
.join(PackageUpdateConfig.package) \
|
||||
.filter(Package.state == PackageState.APPROVED)
|
||||
|
||||
if is_mtm_only:
|
||||
query = query.filter(Package.repo.ilike("%github.com/minetest-mods/%"))
|
||||
|
||||
sort_by = request.args.get("sort")
|
||||
if sort_by == "date":
|
||||
query = query.order_by(db.desc(PackageUpdateConfig.outdated_at))
|
||||
else:
|
||||
sort_by = "score"
|
||||
query = query.order_by(db.desc(Package.score))
|
||||
|
||||
return render_template("todo/outdated.html", current_tab="outdated",
|
||||
outdated_packages=query.all(), sort_by=sort_by, is_mtm_only=is_mtm_only)
|
||||
|
||||
@@ -2,4 +2,4 @@ from flask import Blueprint
|
||||
|
||||
bp = Blueprint("users", __name__)
|
||||
|
||||
from . import githublogin, profile
|
||||
from . import profile, claim, account, settings
|
||||
|
||||
386
app/blueprints/users/account.py
Normal file
386
app/blueprints/users/account.py
Normal file
@@ -0,0 +1,386 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2020 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_login import current_user, login_required, logout_user, login_user
|
||||
from flask_wtf import FlaskForm
|
||||
from sqlalchemy import or_
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import *
|
||||
from app.tasks.emails import send_verify_email, send_anon_email, send_unsubscribe_verify, send_user_email
|
||||
from app.utils import randomString, make_flask_login_password, is_safe_url, check_password_hash, addAuditLog, nonEmptyOrNone
|
||||
from passlib.pwd import genphrase
|
||||
|
||||
from . import bp
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField("Username or email", [InputRequired()])
|
||||
password = PasswordField("Password", [InputRequired(), Length(6, 100)])
|
||||
remember_me = BooleanField("Remember me", default=True)
|
||||
submit = SubmitField("Sign in")
|
||||
|
||||
|
||||
def handle_login(form):
|
||||
def show_safe_err(err):
|
||||
if "@" in username:
|
||||
flash("Incorrect email or password", "danger")
|
||||
else:
|
||||
flash(err, "danger")
|
||||
|
||||
|
||||
username = form.username.data.strip()
|
||||
user = User.query.filter(or_(User.username == username, User.email == username)).first()
|
||||
if user is None:
|
||||
return show_safe_err("User {} does not exist".format(username))
|
||||
|
||||
if not check_password_hash(user.password, form.password.data):
|
||||
return show_safe_err("Incorrect password. Did you set one?")
|
||||
|
||||
if not user.is_active:
|
||||
flash("You need to confirm the registration email", "danger")
|
||||
return
|
||||
|
||||
addAuditLog(AuditSeverity.USER, user, "Logged in using password",
|
||||
url_for("users.profile", username=user.username))
|
||||
db.session.commit()
|
||||
|
||||
login_user(user, remember=form.remember_me.data)
|
||||
flash("Logged in successfully.", "success")
|
||||
|
||||
next = request.args.get("next")
|
||||
if next and not is_safe_url(next):
|
||||
abort(400)
|
||||
|
||||
return redirect(next or url_for("homepage.home"))
|
||||
|
||||
|
||||
@bp.route("/user/login/", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
next = request.args.get("next")
|
||||
if next and not is_safe_url(next):
|
||||
abort(400)
|
||||
|
||||
return redirect(next or url_for("homepage.home"))
|
||||
|
||||
form = LoginForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
ret = handle_login(form)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
if request.method == "GET":
|
||||
form.remember_me.data = True
|
||||
|
||||
|
||||
return render_template("users/login.html", form=form)
|
||||
|
||||
|
||||
@bp.route("/user/logout/", methods=["GET", "POST"])
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
|
||||
class RegisterForm(FlaskForm):
|
||||
username = StringField("Username", [InputRequired()])
|
||||
email = StringField("Email", [InputRequired(), Email()])
|
||||
password = PasswordField("Password", [InputRequired(), Length(6, 100)])
|
||||
submit = SubmitField("Register")
|
||||
|
||||
|
||||
def handle_register(form):
|
||||
user_by_name = User.query.filter(or_(
|
||||
User.username == form.username.data,
|
||||
User.forums_username == form.username.data,
|
||||
User.github_username == form.username.data)).first()
|
||||
if user_by_name:
|
||||
if user_by_name.rank == UserRank.NOT_JOINED and user_by_name.forums_username:
|
||||
flash("An account already exists for that username but hasn't been claimed yet.", "danger")
|
||||
return redirect(url_for("users.claim_forums", username=user_by_name.forums_username))
|
||||
else:
|
||||
flash("That username is already in use, please choose another.", "danger")
|
||||
return
|
||||
|
||||
|
||||
user_by_email = User.query.filter_by(email=form.email.data).first()
|
||||
if user_by_email:
|
||||
send_anon_email.delay(form.email.data, "Email already in use",
|
||||
"We were unable to create the account as the email is already in use by {}. Try a different email address.".format(
|
||||
user_by_email.display_name))
|
||||
flash("Check your email address to verify your account", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
elif EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0:
|
||||
flash("That email address has been unsubscribed/blacklisted, and cannot be used", "danger")
|
||||
return
|
||||
|
||||
user = User(form.username.data, False, form.email.data, make_flask_login_password(form.password.data))
|
||||
user.notification_preferences = UserNotificationPreferences(user)
|
||||
db.session.add(user)
|
||||
|
||||
addAuditLog(AuditSeverity.USER, user, "Registered with email",
|
||||
url_for("users.profile", username=user.username))
|
||||
|
||||
token = randomString(32)
|
||||
|
||||
ver = UserEmailVerification()
|
||||
ver.user = user
|
||||
ver.token = token
|
||||
ver.email = form.email.data
|
||||
db.session.add(ver)
|
||||
db.session.commit()
|
||||
|
||||
send_verify_email.delay(form.email.data, token)
|
||||
|
||||
flash("Check your email address to verify your account", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
|
||||
@bp.route("/user/register/", methods=["GET", "POST"])
|
||||
def register():
|
||||
form = RegisterForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
ret = handle_register(form)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
return render_template("users/register.html", form=form, suggested_password=genphrase(entropy=52, wordset="bip39"))
|
||||
|
||||
|
||||
class ForgotPasswordForm(FlaskForm):
|
||||
email = StringField("Email", [InputRequired(), Email()])
|
||||
submit = SubmitField("Reset Password")
|
||||
|
||||
@bp.route("/user/forgot-password/", methods=["GET", "POST"])
|
||||
def forgot_password():
|
||||
form = ForgotPasswordForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
email = form.email.data
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if user:
|
||||
token = randomString(32)
|
||||
|
||||
addAuditLog(AuditSeverity.USER, user, "(Anonymous) requested a password reset",
|
||||
url_for("users.profile", username=user.username), None)
|
||||
|
||||
ver = UserEmailVerification()
|
||||
ver.user = user
|
||||
ver.token = token
|
||||
ver.email = email
|
||||
ver.is_password_reset = True
|
||||
db.session.add(ver)
|
||||
db.session.commit()
|
||||
|
||||
send_verify_email.delay(form.email.data, token)
|
||||
else:
|
||||
send_anon_email.delay(email, "Unable to find account", """
|
||||
<p>
|
||||
We were unable to perform the password reset as we could not find an account
|
||||
associated with this email.
|
||||
</p>
|
||||
<p>
|
||||
If you weren't expecting to receive this email, then you can safely ignore it.
|
||||
</p>
|
||||
""")
|
||||
|
||||
flash("Check your email address to continue the reset", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
return render_template("users/forgot_password.html", form=form)
|
||||
|
||||
|
||||
class SetPasswordForm(FlaskForm):
|
||||
email = StringField("Email", [Optional(), Email()])
|
||||
password = PasswordField("New password", [InputRequired(), Length(8, 100)])
|
||||
password2 = PasswordField("Verify password", [InputRequired(), Length(8, 100), validators.EqualTo('password', message='Passwords must match')])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
class ChangePasswordForm(FlaskForm):
|
||||
old_password = PasswordField("Old password", [InputRequired(), Length(8, 100)])
|
||||
password = PasswordField("New password", [InputRequired(), Length(8, 100)])
|
||||
password2 = PasswordField("Verify password", [InputRequired(), Length(8, 100), validators.EqualTo('password', message='Passwords must match')])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
def handle_set_password(form):
|
||||
one = form.password.data
|
||||
two = form.password2.data
|
||||
if one != two:
|
||||
flash("Passwords do not much", "danger")
|
||||
return
|
||||
|
||||
addAuditLog(AuditSeverity.USER, current_user, "Changed their password", url_for("users.profile", username=current_user.username))
|
||||
|
||||
current_user.password = make_flask_login_password(form.password.data)
|
||||
|
||||
if hasattr(form, "email"):
|
||||
newEmail = nonEmptyOrNone(form.email.data)
|
||||
if newEmail and newEmail != current_user.email:
|
||||
if EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0:
|
||||
flash("That email address has been unsubscribed/blacklisted, and cannot be used", "danger")
|
||||
return
|
||||
|
||||
token = randomString(32)
|
||||
|
||||
ver = UserEmailVerification()
|
||||
ver.user = current_user
|
||||
ver.token = token
|
||||
ver.email = newEmail
|
||||
db.session.add(ver)
|
||||
|
||||
db.session.commit()
|
||||
flash("Your password has been changed successfully.", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
|
||||
@bp.route("/user/change-password/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def change_password():
|
||||
form = ChangePasswordForm(request.form)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if check_password_hash(current_user.password, form.old_password.data):
|
||||
ret = handle_set_password(form)
|
||||
if ret:
|
||||
return ret
|
||||
else:
|
||||
flash("Old password is incorrect", "danger")
|
||||
|
||||
return render_template("users/change_set_password.html", form=form,
|
||||
suggested_password=genphrase(entropy=52, wordset="bip39"))
|
||||
|
||||
|
||||
@bp.route("/user/set-password/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def set_password():
|
||||
if current_user.password:
|
||||
return redirect(url_for("users.change_password"))
|
||||
|
||||
form = SetPasswordForm(request.form)
|
||||
if current_user.email is None:
|
||||
form.email.validators = [InputRequired(), Email()]
|
||||
|
||||
if form.validate_on_submit():
|
||||
ret = handle_set_password(form)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
return render_template("users/change_set_password.html", form=form, optional=request.args.get("optional"),
|
||||
suggested_password=genphrase(entropy=52, wordset="bip39"))
|
||||
|
||||
|
||||
@bp.route("/user/verify/")
|
||||
def verify_email():
|
||||
token = request.args.get("token")
|
||||
ver : UserEmailVerification = UserEmailVerification.query.filter_by(token=token).first()
|
||||
if ver is None:
|
||||
flash("Unknown verification token!", "danger")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
user = ver.user
|
||||
|
||||
addAuditLog(AuditSeverity.USER, user, "Confirmed their email",
|
||||
url_for("users.profile", username=user.username))
|
||||
|
||||
was_activating = not user.is_active
|
||||
|
||||
if ver.email and user.email != ver.email:
|
||||
if User.query.filter_by(email=ver.email).count() > 0:
|
||||
flash("Another user is already using that email", "danger")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
flash("Confirmed email change", "success")
|
||||
|
||||
if user.email:
|
||||
send_user_email.delay(user.email,
|
||||
"Email address changed",
|
||||
"Your email address has changed. If you didn't request this, please contact an administrator.")
|
||||
|
||||
user.is_active = True
|
||||
user.email = ver.email
|
||||
|
||||
db.session.delete(ver)
|
||||
db.session.commit()
|
||||
|
||||
if ver.is_password_reset:
|
||||
login_user(user)
|
||||
user.password = None
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("users.set_password"))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("users.profile", username=current_user.username))
|
||||
elif was_activating:
|
||||
flash("You may now log in", "success")
|
||||
return redirect(url_for("users.login"))
|
||||
else:
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
|
||||
class UnsubscribeForm(FlaskForm):
|
||||
email = StringField("Email", [InputRequired(), Email()])
|
||||
submit = SubmitField("Send")
|
||||
|
||||
|
||||
def unsubscribe_verify():
|
||||
form = UnsubscribeForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
email = form.email.data
|
||||
sub = EmailSubscription.query.filter_by(email=email).first()
|
||||
if not sub:
|
||||
sub = EmailSubscription(email)
|
||||
db.session.add(sub)
|
||||
|
||||
sub.token = randomString(32)
|
||||
db.session.commit()
|
||||
send_unsubscribe_verify.delay(form.email.data)
|
||||
|
||||
flash("Check your email address to continue the unsubscribe", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
return render_template("users/unsubscribe.html", form=form)
|
||||
|
||||
|
||||
def unsubscribe_manage(sub: EmailSubscription):
|
||||
user = User.query.filter_by(email=sub.email).first()
|
||||
|
||||
if request.method == "POST":
|
||||
if user:
|
||||
user.email = None
|
||||
|
||||
sub.blacklisted = True
|
||||
db.session.commit()
|
||||
|
||||
flash("That email is now blacklisted. Please contact an admin if you wish to undo this.", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
return render_template("users/unsubscribe.html", user=user)
|
||||
|
||||
|
||||
@bp.route("/unsubscribe/", methods=["GET", "POST"])
|
||||
def unsubscribe():
|
||||
token = request.args.get("token")
|
||||
if token:
|
||||
sub = EmailSubscription.query.filter_by(token=token).first()
|
||||
if sub:
|
||||
return unsubscribe_manage(sub)
|
||||
|
||||
return unsubscribe_verify()
|
||||
120
app/blueprints/users/claim.py
Normal file
120
app/blueprints/users/claim.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from . import bp
|
||||
from flask import redirect, render_template, session, request, flash, url_for
|
||||
from app.models import db, User, UserRank
|
||||
from app.utils import randomString, login_user_set_active
|
||||
from app.tasks.forumtasks import checkForumAccount
|
||||
from app.utils.phpbbparser import getProfile
|
||||
import re
|
||||
|
||||
|
||||
def check_username(username):
|
||||
return username is not None and len(username) >= 2 and re.match("^[A-Za-z0-9._-]*$", username)
|
||||
|
||||
|
||||
|
||||
@bp.route("/user/claim/", methods=["GET", "POST"])
|
||||
def claim():
|
||||
return render_template("users/claim.html")
|
||||
|
||||
|
||||
@bp.route("/user/claim-forums/", methods=["GET", "POST"])
|
||||
def claim_forums():
|
||||
username = request.args.get("username")
|
||||
if username is None:
|
||||
username = ""
|
||||
else:
|
||||
method = request.args.get("method")
|
||||
|
||||
if not check_username(username):
|
||||
flash("Invalid username - must only contain A-Za-z0-9._. Consider contacting an admin", "danger")
|
||||
return redirect(url_for("users.claim_forums"))
|
||||
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||
flash("User has already been claimed", "danger")
|
||||
return redirect(url_for("users.claim_forums"))
|
||||
elif method == "github":
|
||||
if user is None or user.github_username is None:
|
||||
flash("Unable to get GitHub username for user", "danger")
|
||||
return redirect(url_for("users.claim_forums", username=username))
|
||||
else:
|
||||
return redirect(url_for("github.start"))
|
||||
|
||||
if "forum_token" in session:
|
||||
token = session["forum_token"]
|
||||
else:
|
||||
token = randomString(12)
|
||||
session["forum_token"] = token
|
||||
|
||||
if request.method == "POST":
|
||||
ctype = request.form.get("claim_type")
|
||||
username = request.form.get("username")
|
||||
|
||||
if not check_username(username):
|
||||
flash("Invalid username - must only contain A-Za-z0-9._. Consider contacting an admin", "danger")
|
||||
elif ctype == "github":
|
||||
task = checkForumAccount.delay(username)
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim_forums", username=username, method="github")))
|
||||
elif ctype == "forum":
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||
flash("That user has already been claimed!", "danger")
|
||||
return redirect(url_for("users.claim_forums"))
|
||||
|
||||
# Get signature
|
||||
sig = None
|
||||
try:
|
||||
profile = getProfile("https://forum.minetest.net", username)
|
||||
sig = profile.signature if profile else None
|
||||
except IOError as e:
|
||||
if hasattr(e, 'message'):
|
||||
message = e.message
|
||||
else:
|
||||
message = str(e)
|
||||
|
||||
flash("Error whilst attempting to access forums: " + message, "danger")
|
||||
return redirect(url_for("users.claim_forums", username=username))
|
||||
|
||||
if profile is None:
|
||||
flash("Unable to get forum signature - does the user exist?", "danger")
|
||||
return redirect(url_for("users.claim_forums", username=username))
|
||||
|
||||
# Look for key
|
||||
if sig and token in sig:
|
||||
# Try getting again to fix crash
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user is None:
|
||||
user = User(username)
|
||||
user.forums_username = username
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
if login_user_set_active(user, remember=True):
|
||||
return redirect(url_for("users.set_password"))
|
||||
else:
|
||||
flash("Unable to login as user", "danger")
|
||||
return redirect(url_for("users.claim_forums", username=username))
|
||||
|
||||
else:
|
||||
flash("Could not find the key in your signature!", "danger")
|
||||
return redirect(url_for("users.claim_forums", username=username))
|
||||
else:
|
||||
flash("Unknown claim type", "danger")
|
||||
|
||||
return render_template("users/claim_forums.html", username=username, key="cdb_" + token)
|
||||
@@ -1,74 +0,0 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from flask_login import login_user, logout_user
|
||||
from sqlalchemy import func
|
||||
import flask_menu as menu
|
||||
from flask_github import GitHub
|
||||
from . import bp
|
||||
from app import github
|
||||
from app.models import *
|
||||
from app.utils import loginUser
|
||||
|
||||
@bp.route("/user/github/start/")
|
||||
def github_signin():
|
||||
return github.authorize("")
|
||||
|
||||
@bp.route("/user/github/callback/")
|
||||
@github.authorized_handler
|
||||
def github_authorized(oauth_token):
|
||||
next_url = request.args.get("next")
|
||||
if oauth_token is None:
|
||||
flash("Authorization failed [err=gh-oauth-login-failed]", "danger")
|
||||
return redirect(url_for("user.login"))
|
||||
|
||||
import requests
|
||||
|
||||
# Get Github username
|
||||
url = "https://api.github.com/user"
|
||||
r = requests.get(url, headers={"Authorization": "token " + oauth_token})
|
||||
username = r.json()["login"]
|
||||
|
||||
# Get user by github username
|
||||
userByGithub = User.query.filter(func.lower(User.github_username) == func.lower(username)).first()
|
||||
|
||||
# If logged in, connect
|
||||
if current_user and current_user.is_authenticated:
|
||||
if userByGithub is None:
|
||||
current_user.github_username = username
|
||||
db.session.commit()
|
||||
flash("Linked github to account", "success")
|
||||
return redirect(url_for("homepage.home"))
|
||||
else:
|
||||
flash("Github account is already associated with another user", "danger")
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
# If not logged in, log in
|
||||
else:
|
||||
if userByGithub is None:
|
||||
flash("Unable to find an account for that Github user", "error")
|
||||
return redirect(url_for("users.claim"))
|
||||
elif loginUser(userByGithub):
|
||||
if current_user.password is None:
|
||||
return redirect(next_url or url_for("users.set_password", optional=True))
|
||||
else:
|
||||
return redirect(next_url or url_for("homepage.home"))
|
||||
else:
|
||||
flash("Authorization failed [err=gh-login-failed]", "danger")
|
||||
return redirect(url_for("user.login"))
|
||||
@@ -1,115 +1,61 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask import *
|
||||
from flask_user import *
|
||||
from flask_login import login_user, logout_user
|
||||
from app import markdown
|
||||
from . import bp
|
||||
from app.models import *
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.utils import randomString, loginUser, rank_required
|
||||
from app.tasks.forumtasks import checkForumAccount
|
||||
from app.tasks.emails import sendVerifyEmail, sendEmailRaw
|
||||
from app.tasks.phpbbparser import getProfile
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import func
|
||||
|
||||
# Define the User profile form
|
||||
class UserProfileForm(FlaskForm):
|
||||
display_name = StringField("Display name", [Optional(), Length(2, 20)])
|
||||
email = StringField("Email", [Optional(), Email()], filters = [lambda x: x or None])
|
||||
website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
|
||||
submit = SubmitField("Save")
|
||||
from app.models import *
|
||||
from app.tasks.forumtasks import checkForumAccount
|
||||
from . import bp
|
||||
|
||||
|
||||
@bp.route("/users/", methods=["GET"])
|
||||
def list_all():
|
||||
users = User.query.order_by(db.desc(User.rank), db.asc(User.display_name)).all()
|
||||
users = db.session.query(User, func.count(Package.id)) \
|
||||
.select_from(User).outerjoin(Package) \
|
||||
.order_by(db.desc(User.rank), db.asc(User.display_name)) \
|
||||
.group_by(User.id).all()
|
||||
|
||||
return render_template("users/list.html", users=users)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/", methods=["GET", "POST"])
|
||||
@bp.route("/user/forum/<username>/")
|
||||
def by_forums_username(username):
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user:
|
||||
return redirect(url_for("users.profile", username=user.username))
|
||||
|
||||
return render_template("users/forums_no_such_user.html", username=username)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/")
|
||||
def profile(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
form = None
|
||||
if user.checkPerm(current_user, Permission.CHANGE_DNAME) or \
|
||||
user.checkPerm(current_user, Permission.CHANGE_EMAIL) or \
|
||||
user.checkPerm(current_user, Permission.CHANGE_RANK):
|
||||
# Initialize form
|
||||
form = UserProfileForm(formdata=request.form, obj=user)
|
||||
|
||||
# Process valid POST
|
||||
if request.method=="POST" and form.validate():
|
||||
# Copy form fields to user_profile fields
|
||||
if user.checkPerm(current_user, Permission.CHANGE_DNAME):
|
||||
user.display_name = form["display_name"].data
|
||||
user.website_url = form["website_url"].data
|
||||
user.donate_url = form["donate_url"].data
|
||||
|
||||
if user.checkPerm(current_user, Permission.CHANGE_RANK):
|
||||
newRank = form["rank"].data
|
||||
if current_user.rank.atLeast(newRank):
|
||||
user.rank = form["rank"].data
|
||||
else:
|
||||
flash("Can't promote a user to a rank higher than yourself!", "error")
|
||||
|
||||
if user.checkPerm(current_user, Permission.CHANGE_EMAIL):
|
||||
newEmail = form["email"].data
|
||||
if newEmail != user.email and newEmail.strip() != "":
|
||||
token = randomString(32)
|
||||
|
||||
ver = UserEmailVerification()
|
||||
ver.user = user
|
||||
ver.token = token
|
||||
ver.email = newEmail
|
||||
db.session.add(ver)
|
||||
db.session.commit()
|
||||
|
||||
task = sendVerifyEmail.delay(newEmail, token)
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=username)))
|
||||
|
||||
# Save user_profile
|
||||
db.session.commit()
|
||||
|
||||
# Redirect to home page
|
||||
return redirect(url_for("users.profile", username=username))
|
||||
|
||||
packages = user.packages.filter_by(soft_deleted=False)
|
||||
packages = user.packages.filter(Package.state != PackageState.DELETED)
|
||||
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
|
||||
packages = packages.filter_by(approved=True)
|
||||
packages = packages.filter_by(state=PackageState.APPROVED)
|
||||
packages = packages.order_by(db.asc(Package.title))
|
||||
|
||||
topics_to_add = None
|
||||
if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR):
|
||||
topics_to_add = ForumTopic.query \
|
||||
.filter_by(author_id=user.id) \
|
||||
.filter(~ db.exists().where(Package.forums==ForumTopic.topic_id)) \
|
||||
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
|
||||
.all()
|
||||
|
||||
# Process GET or invalid POST
|
||||
return render_template("users/profile.html",
|
||||
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
|
||||
return render_template("users/profile.html", user=user, packages=packages)
|
||||
|
||||
|
||||
@bp.route("/users/<username>/check/", methods=["POST"])
|
||||
@@ -129,181 +75,3 @@ def user_check(username):
|
||||
next_url = url_for("users.profile", username=username)
|
||||
|
||||
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
||||
|
||||
|
||||
class SendEmailForm(FlaskForm):
|
||||
subject = StringField("Subject", [InputRequired(), Length(1, 300)])
|
||||
text = TextAreaField("Message", [InputRequired()])
|
||||
submit = SubmitField("Send")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/email/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.MODERATOR)
|
||||
def send_email(username):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
abort(404)
|
||||
|
||||
next_url = url_for("users.profile", username=user.username)
|
||||
|
||||
if user.email is None:
|
||||
flash("User has no email address!", "error")
|
||||
return redirect(next_url)
|
||||
|
||||
form = SendEmailForm(request.form)
|
||||
if form.validate_on_submit():
|
||||
text = form.text.data
|
||||
html = markdown(text)
|
||||
task = sendEmailRaw.delay([user.email], form.subject.data, text, html)
|
||||
return redirect(url_for("tasks.check", id=task.id, r=next_url))
|
||||
|
||||
return render_template("users/send_email.html", form=form)
|
||||
|
||||
|
||||
|
||||
class SetPasswordForm(FlaskForm):
|
||||
email = StringField("Email", [Optional(), Email()])
|
||||
password = PasswordField("New password", [InputRequired(), Length(2, 100)])
|
||||
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
@bp.route("/user/set-password/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def set_password():
|
||||
if current_user.password is not None:
|
||||
return redirect(url_for("user.change_password"))
|
||||
|
||||
form = SetPasswordForm(request.form)
|
||||
if current_user.email == None:
|
||||
form.email.validators = [InputRequired(), Email()]
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
one = form.password.data
|
||||
two = form.password2.data
|
||||
if one == two:
|
||||
# Hash password
|
||||
hashed_password = user_manager.hash_password(form.password.data)
|
||||
|
||||
# Change password
|
||||
user_manager.update_password(current_user, hashed_password)
|
||||
|
||||
# Send 'password_changed' email
|
||||
if user_manager.enable_email and user_manager.send_password_changed_email and current_user.email:
|
||||
emails.send_password_changed_email(current_user)
|
||||
|
||||
# Send password_changed signal
|
||||
signals.user_changed_password.send(current_app._get_current_object(), user=current_user)
|
||||
|
||||
# Prepare one-time system message
|
||||
flash('Your password has been changed successfully.', 'success')
|
||||
|
||||
newEmail = form["email"].data
|
||||
if newEmail != current_user.email and newEmail.strip() != "":
|
||||
token = randomString(32)
|
||||
|
||||
ver = UserEmailVerification()
|
||||
ver.user = current_user
|
||||
ver.token = token
|
||||
ver.email = newEmail
|
||||
db.session.add(ver)
|
||||
db.session.commit()
|
||||
|
||||
task = sendVerifyEmail.delay(newEmail, token)
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username)))
|
||||
else:
|
||||
return redirect(url_for("users.profile", username=current_user.username))
|
||||
else:
|
||||
flash("Passwords do not match", "error")
|
||||
|
||||
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
|
||||
|
||||
|
||||
@bp.route("/user/claim/", methods=["GET", "POST"])
|
||||
def claim():
|
||||
username = request.args.get("username")
|
||||
if username is None:
|
||||
username = ""
|
||||
else:
|
||||
method = request.args.get("method")
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||
flash("User has already been claimed", "error")
|
||||
return redirect(url_for("users.claim"))
|
||||
elif user is None and method == "github":
|
||||
flash("Unable to get Github username for user", "error")
|
||||
return redirect(url_for("users.claim"))
|
||||
elif user is None:
|
||||
flash("Unable to find that user", "error")
|
||||
return redirect(url_for("users.claim"))
|
||||
|
||||
if user is not None and method == "github":
|
||||
return redirect(url_for("users.github_signin"))
|
||||
|
||||
token = None
|
||||
if "forum_token" in session:
|
||||
token = session["forum_token"]
|
||||
else:
|
||||
token = randomString(32)
|
||||
session["forum_token"] = token
|
||||
|
||||
if request.method == "POST":
|
||||
ctype = request.form.get("claim_type")
|
||||
username = request.form.get("username")
|
||||
|
||||
if username is None or len(username.strip()) < 2:
|
||||
flash("Invalid username", "error")
|
||||
elif ctype == "github":
|
||||
task = checkForumAccount.delay(username)
|
||||
return redirect(url_for("tasks.check", id=task.id, r=url_for("users.claim", username=username, method="github")))
|
||||
elif ctype == "forum":
|
||||
user = User.query.filter_by(forums_username=username).first()
|
||||
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
|
||||
flash("That user has already been claimed!", "error")
|
||||
return redirect(url_for("users.claim"))
|
||||
|
||||
# Get signature
|
||||
sig = None
|
||||
try:
|
||||
profile = getProfile("https://forum.minetest.net", username)
|
||||
sig = profile.signature
|
||||
except IOError:
|
||||
flash("Unable to get forum signature - does the user exist?", "error")
|
||||
return redirect(url_for("users.claim", username=username))
|
||||
|
||||
# Look for key
|
||||
if token in sig:
|
||||
if user is None:
|
||||
user = User(username)
|
||||
user.forums_username = username
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
if loginUser(user):
|
||||
return redirect(url_for("users.set_password"))
|
||||
else:
|
||||
flash("Unable to login as user", "error")
|
||||
return redirect(url_for("users.claim", username=username))
|
||||
|
||||
else:
|
||||
flash("Could not find the key in your signature!", "error")
|
||||
return redirect(url_for("users.claim", username=username))
|
||||
else:
|
||||
flash("Unknown claim type", "error")
|
||||
|
||||
return render_template("users/claim.html", username=username, key=token)
|
||||
|
||||
@bp.route("/users/verify/")
|
||||
def verify_email():
|
||||
token = request.args.get("token")
|
||||
ver = UserEmailVerification.query.filter_by(token=token).first()
|
||||
if ver is None:
|
||||
flash("Unknown verification token!", "error")
|
||||
else:
|
||||
ver.user.email = ver.email
|
||||
db.session.delete(ver)
|
||||
db.session.commit()
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("users.profile", username=current_user.username))
|
||||
else:
|
||||
return redirect(url_for("homepage.home"))
|
||||
|
||||
257
app/blueprints/users/settings.py
Normal file
257
app/blueprints/users/settings.py
Normal file
@@ -0,0 +1,257 @@
|
||||
from flask import *
|
||||
from flask_login import current_user, login_required, logout_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
from app.models import *
|
||||
from app.utils import nonEmptyOrNone, addAuditLog, randomString, rank_required
|
||||
from app.tasks.emails import send_verify_email
|
||||
from . import bp
|
||||
|
||||
|
||||
def get_setting_tabs(user):
|
||||
return [
|
||||
{
|
||||
"id": "edit_profile",
|
||||
"title": "Edit Profile",
|
||||
"url": url_for("users.profile_edit", username=user.username)
|
||||
},
|
||||
{
|
||||
"id": "account",
|
||||
"title": "Account and Security",
|
||||
"url": url_for("users.account", username=user.username)
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"title": "Email and Notifications",
|
||||
"url": url_for("users.email_notifications", username=user.username)
|
||||
},
|
||||
{
|
||||
"id": "api_tokens",
|
||||
"title": "API Tokens",
|
||||
"url": url_for("api.list_tokens", username=user.username)
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class UserProfileForm(FlaskForm):
|
||||
website_url = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
donate_url = StringField("Donation URL", [Optional(), URL()], filters = [lambda x: x or None])
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/settings/profile/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def profile_edit(username):
|
||||
user : User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if not user.can_see_edit_profile(current_user):
|
||||
flash("Permission denied", "danger")
|
||||
return redirect(url_for("users.profile", username=username))
|
||||
|
||||
form = UserProfileForm(obj=user)
|
||||
if form.validate_on_submit():
|
||||
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
|
||||
addAuditLog(severity, current_user, "Edited {}'s profile".format(user.display_name),
|
||||
url_for("users.profile", username=username))
|
||||
|
||||
if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS):
|
||||
user.website_url = form["website_url"].data
|
||||
user.donate_url = form["donate_url"].data
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("users.profile", username=username))
|
||||
|
||||
# Process GET or invalid POST
|
||||
return render_template("users/profile_edit.html", user=user, form=form, tabs=get_setting_tabs(user), current_tab="edit_profile")
|
||||
|
||||
|
||||
def make_settings_form():
|
||||
attrs = {
|
||||
"email": StringField("Email", [Optional(), Email()]),
|
||||
"submit": SubmitField("Save")
|
||||
}
|
||||
|
||||
for notificationType in NotificationType:
|
||||
key = "pref_" + notificationType.toName()
|
||||
attrs[key] = BooleanField("")
|
||||
attrs[key + "_digest"] = BooleanField("")
|
||||
|
||||
return type("SettingsForm", (FlaskForm,), attrs)
|
||||
|
||||
SettingsForm = make_settings_form()
|
||||
|
||||
|
||||
def handle_email_notifications(user, prefs: UserNotificationPreferences, is_new, form):
|
||||
for notificationType in NotificationType:
|
||||
field_email = getattr(form, "pref_" + notificationType.toName()).data
|
||||
field_digest = getattr(form, "pref_" + notificationType.toName() + "_digest").data or field_email
|
||||
prefs.set_can_email(notificationType, field_email)
|
||||
prefs.set_can_digest(notificationType, field_digest)
|
||||
|
||||
if is_new:
|
||||
db.session.add(prefs)
|
||||
|
||||
if user.checkPerm(current_user, Permission.CHANGE_EMAIL):
|
||||
newEmail = form.email.data
|
||||
if newEmail and newEmail != user.email and newEmail.strip() != "":
|
||||
if EmailSubscription.query.filter_by(email=form.email.data, blacklisted=True).count() > 0:
|
||||
flash("That email address has been unsubscribed/blacklisted, and cannot be used", "danger")
|
||||
return
|
||||
|
||||
token = randomString(32)
|
||||
|
||||
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
|
||||
|
||||
msg = "Changed email of {}".format(user.display_name)
|
||||
addAuditLog(severity, current_user, msg, url_for("users.profile", username=user.username))
|
||||
|
||||
ver = UserEmailVerification()
|
||||
ver.user = user
|
||||
ver.token = token
|
||||
ver.email = newEmail
|
||||
db.session.add(ver)
|
||||
db.session.commit()
|
||||
|
||||
flash("Check your email to confirm it", "success")
|
||||
|
||||
send_verify_email.delay(newEmail, token)
|
||||
return redirect(url_for("users.email_notifications", username=user.username))
|
||||
|
||||
db.session.commit()
|
||||
return redirect(url_for("users.email_notifications", username=user.username))
|
||||
|
||||
|
||||
@bp.route("/user/settings/email/")
|
||||
@bp.route("/users/<username>/settings/email/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def email_notifications(username=None):
|
||||
if username is None:
|
||||
return redirect(url_for("users.email_notifications", username=current_user.username))
|
||||
|
||||
user: User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if not user.checkPerm(current_user, Permission.CHANGE_EMAIL):
|
||||
abort(403)
|
||||
|
||||
is_new = False
|
||||
prefs = user.notification_preferences
|
||||
if prefs is None:
|
||||
is_new = True
|
||||
prefs = UserNotificationPreferences(user)
|
||||
|
||||
data = {}
|
||||
types = []
|
||||
for notificationType in NotificationType:
|
||||
types.append(notificationType)
|
||||
data["pref_" + notificationType.toName()] = prefs.get_can_email(notificationType)
|
||||
data["pref_" + notificationType.toName() + "_digest"] = prefs.get_can_digest(notificationType)
|
||||
|
||||
data["email"] = user.email
|
||||
|
||||
form = SettingsForm(data=data)
|
||||
if form.validate_on_submit():
|
||||
ret = handle_email_notifications(user, prefs, is_new, form)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
return render_template("users/settings_email.html",
|
||||
form=form, user=user, types=types, is_new=is_new,
|
||||
tabs=get_setting_tabs(user), current_tab="notifications")
|
||||
|
||||
|
||||
class UserAccountForm(FlaskForm):
|
||||
display_name = StringField("Display name", [Optional(), Length(2, 100)])
|
||||
forums_username = StringField("Forums Username", [Optional(), Length(2, 50)])
|
||||
github_username = StringField("GitHub Username", [Optional(), Length(2, 50)])
|
||||
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce,
|
||||
default=UserRank.NEW_MEMBER)
|
||||
submit = SubmitField("Save")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/settings/account/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def account(username):
|
||||
user : User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if not user.can_see_edit_profile(current_user):
|
||||
flash("Permission denied", "danger")
|
||||
return redirect(url_for("users.profile", username=username))
|
||||
|
||||
can_edit_account_settings = user.checkPerm(current_user, Permission.CHANGE_USERNAMES) or \
|
||||
user.checkPerm(current_user, Permission.CHANGE_RANK)
|
||||
form = UserAccountForm(obj=user) if can_edit_account_settings else None
|
||||
if form and form.validate_on_submit():
|
||||
severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION
|
||||
addAuditLog(severity, current_user, "Edited {}'s profile".format(user.display_name),
|
||||
url_for("users.profile", username=username))
|
||||
|
||||
# Copy form fields to user_profile fields
|
||||
if user.checkPerm(current_user, Permission.CHANGE_USERNAMES):
|
||||
user.display_name = form.display_name.data
|
||||
user.forums_username = nonEmptyOrNone(form.forums_username.data)
|
||||
user.github_username = nonEmptyOrNone(form.github_username.data)
|
||||
|
||||
if user.checkPerm(current_user, Permission.CHANGE_RANK):
|
||||
newRank = form["rank"].data
|
||||
if current_user.rank.atLeast(newRank):
|
||||
if newRank != user.rank:
|
||||
user.rank = form["rank"].data
|
||||
msg = "Set rank of {} to {}".format(user.display_name, user.rank.getTitle())
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg,
|
||||
url_for("users.profile", username=username))
|
||||
else:
|
||||
flash("Can't promote a user to a rank higher than yourself!", "danger")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("users.account", username=username))
|
||||
|
||||
return render_template("users/account.html", user=user, form=form, tabs=get_setting_tabs(user), current_tab="account")
|
||||
|
||||
|
||||
@bp.route("/users/<username>/delete/", methods=["GET", "POST"])
|
||||
@rank_required(UserRank.ADMIN)
|
||||
def delete(username):
|
||||
user: User = User.query.filter_by(username=username).first()
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
if user.rank.atLeast(UserRank.MODERATOR):
|
||||
flash("Users with moderator rank or above cannot be deleted", "danger")
|
||||
return redirect(url_for("users.account", username=username))
|
||||
|
||||
if request.method == "GET":
|
||||
return render_template("users/delete.html", user=user, can_delete=user.can_delete())
|
||||
|
||||
if user.can_delete():
|
||||
msg = "Deleted user {}".format(user.username)
|
||||
flash(msg, "success")
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None)
|
||||
|
||||
db.session.delete(user)
|
||||
else:
|
||||
user.replies.delete()
|
||||
for thread in user.threads.all():
|
||||
db.session.delete(thread)
|
||||
user.email = None
|
||||
user.rank = UserRank.NOT_JOINED
|
||||
|
||||
msg = "Deactivated user {}".format(user.username)
|
||||
flash(msg, "success")
|
||||
addAuditLog(AuditSeverity.MODERATION, current_user, msg, None)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if user == current_user:
|
||||
logout_user()
|
||||
|
||||
return redirect(url_for("homepage.home"))
|
||||
386
app/default_data.py
Normal file
386
app/default_data.py
Normal file
@@ -0,0 +1,386 @@
|
||||
from .models import *
|
||||
from .utils import make_flask_login_password
|
||||
|
||||
|
||||
def populate(session):
|
||||
admin_user = User("rubenwardy")
|
||||
admin_user.is_active = True
|
||||
admin_user.password = make_flask_login_password("tuckfrump")
|
||||
admin_user.github_username = "rubenwardy"
|
||||
admin_user.forums_username = "rubenwardy"
|
||||
admin_user.rank = UserRank.ADMIN
|
||||
session.add(admin_user)
|
||||
|
||||
session.add(MinetestRelease("None", 0))
|
||||
session.add(MinetestRelease("0.4.16/17", 32))
|
||||
session.add(MinetestRelease("5.0", 37))
|
||||
session.add(MinetestRelease("5.1", 38))
|
||||
|
||||
tags = {}
|
||||
for tag in ["Inventory", "Mapgen", "Building",
|
||||
"Mobs and NPCs", "Tools", "Player effects",
|
||||
"Environment", "Transport", "Maintenance", "Plants and farming",
|
||||
"PvP", "PvE", "Survival", "Creative", "Puzzle", "Multiplayer", "Singleplayer"]:
|
||||
row = Tag(tag)
|
||||
tags[row.name] = row
|
||||
session.add(row)
|
||||
|
||||
licenses = {}
|
||||
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
|
||||
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
|
||||
"CC-BY", "MIT", "ZLib", "Other (Free)"]:
|
||||
row = License(license)
|
||||
licenses[row.name] = row
|
||||
session.add(row)
|
||||
|
||||
for license in ["CC-BY-NC-SA", "Other (Non-free)"]:
|
||||
row = License(license, False)
|
||||
licenses[row.name] = row
|
||||
session.add(row)
|
||||
|
||||
|
||||
def populate_test_data(session):
|
||||
licenses = { x.name : x for x in License.query.all() }
|
||||
tags = { x.name : x for x in Tag.query.all() }
|
||||
admin_user = User.query.filter_by(rank=UserRank.ADMIN).first()
|
||||
v4 = MinetestRelease.query.filter_by(protocol=32).first()
|
||||
v50 = MinetestRelease.query.filter_by(protocol=37).first()
|
||||
v51 = MinetestRelease.query.filter_by(protocol=38).first()
|
||||
|
||||
ez = User("Shara")
|
||||
ez.github_username = "Ezhh"
|
||||
ez.forums_username = "Shara"
|
||||
ez.rank = UserRank.EDITOR
|
||||
session.add(ez)
|
||||
|
||||
not1 = Notification(admin_user, ez, "Awards approved", "/packages/rubenwardy/awards/")
|
||||
session.add(not1)
|
||||
|
||||
jeija = User("Jeija")
|
||||
jeija.github_username = "Jeija"
|
||||
jeija.forums_username = "Jeija"
|
||||
session.add(jeija)
|
||||
|
||||
|
||||
mod = Package()
|
||||
mod.state = PackageState.APPROVED
|
||||
mod.name = "alpha"
|
||||
mod.title = "Alpha Test"
|
||||
mod.license = licenses["MIT"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = admin_user
|
||||
mod.tags.append(tags["mapgen"])
|
||||
mod.tags.append(tags["environment"])
|
||||
mod.repo = "https://github.com/ezhh/other_worlds"
|
||||
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
|
||||
mod.forums = 16015
|
||||
mod.short_desc = "The content library should not be used yet as it is still in alpha"
|
||||
mod.desc = "This is the long desc"
|
||||
session.add(mod)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
|
||||
rel.approved = True
|
||||
session.add(rel)
|
||||
|
||||
mod1 = Package()
|
||||
mod1.state = PackageState.APPROVED
|
||||
mod1.name = "awards"
|
||||
mod1.title = "Awards"
|
||||
mod1.license = licenses["LGPLv2.1"]
|
||||
mod1.media_license = licenses["MIT"]
|
||||
mod1.type = PackageType.MOD
|
||||
mod1.author = admin_user
|
||||
mod1.tags.append(tags["player_effects"])
|
||||
mod1.repo = "https://github.com/rubenwardy/awards"
|
||||
mod1.issueTracker = "https://github.com/rubenwardy/awards/issues"
|
||||
mod1.forums = 4870
|
||||
mod1.short_desc = "Adds achievements and an API to register new ones."
|
||||
mod1.desc = """
|
||||
Majority of awards are back ported from Calinou's old fork in Carbone, under same license.
|
||||
|
||||
```lua
|
||||
awards.register_achievement("award_mesefind",{
|
||||
title = "First Mese Find",
|
||||
description = "Found some Mese!",
|
||||
trigger = {
|
||||
type = "dig", -- award is given when
|
||||
node = "default:mese", -- this type of node has been dug
|
||||
target = 1, -- this number of times
|
||||
},
|
||||
})
|
||||
```
|
||||
"""
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod1
|
||||
rel.min_rel = v51
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "https://github.com/rubenwardy/awards/archive/master.zip"
|
||||
rel.approved = True
|
||||
session.add(rel)
|
||||
|
||||
mod2 = Package()
|
||||
mod2.state = PackageState.APPROVED
|
||||
mod2.name = "mesecons"
|
||||
mod2.title = "Mesecons"
|
||||
mod2.tags.append(tags["tools"])
|
||||
mod2.type = PackageType.MOD
|
||||
mod2.license = licenses["LGPLv3"]
|
||||
mod2.media_license = licenses["MIT"]
|
||||
mod2.author = jeija
|
||||
mod2.repo = "https://github.com/minetest-mods/mesecons/"
|
||||
mod2.issueTracker = "https://github.com/minetest-mods/mesecons/issues"
|
||||
mod2.forums = 628
|
||||
mod2.short_desc = "Mesecons adds everything digital, from all kinds of sensors, switches, solar panels, detectors, pistons, lamps, sound blocks to advanced digital circuitry like logic gates and programmable blocks."
|
||||
mod2.desc = """
|
||||
MESECONS by Jeija and contributors
|
||||
|
||||
Mezzee-what?
|
||||
------------
|
||||
[Mesecons](http://mesecons.net/)! They're yellow, they're conductive, and they'll add a whole new dimension to Minetest's gameplay.
|
||||
|
||||
Mesecons is a mod for [Minetest](http://minetest.net/) that implements a ton of items related to digital circuitry, such as wires, buttons, lights, and even programmable controllers. Among other things, there are also pistons, solar panels, pressure plates, and note blocks.
|
||||
|
||||
Mesecons has a similar goal to Redstone in Minecraft, but works in its own way, with different rules and mechanics.
|
||||
|
||||
OK, I want in.
|
||||
--------------
|
||||
Go get it!
|
||||
|
||||
[DOWNLOAD IT NOW](https://github.com/minetest-mods/mesecons/archive/master.zip)
|
||||
|
||||
Now go ahead and install it like any other Minetest mod. Don't know how? Check out [the wonderful page about it](http://wiki.minetest.com/wiki/Mods) over at the Minetest Wiki. For your convenience, here's a quick summary:
|
||||
|
||||
1. If Mesecons is still in a ZIP file, extract the folder inside to somewhere on the computer.
|
||||
2. Make sure that when you open the folder, you can directly find `README.md` in the listing. If you just see another folder, move that folder up one level and delete the old one.
|
||||
3. Open up the Minetest mods folder - usually `/mods/`. If you see the `minetest` or folder inside of that, that is your mod folder instead.
|
||||
4. Copy the Mesecons folder into the mods folder.
|
||||
|
||||
Don't like some parts of Mesecons? Open up the Mesecons folder and delete the subfolder containing the mod you don't want. If you didn't want movestones, for example, all you have to do is delete the `mesecons_movestones` folder and they will no longer be available.
|
||||
|
||||
There are no dependencies - it will work right after installing!
|
||||
|
||||
How do I use this thing?
|
||||
------------------------
|
||||
How about a [quick overview video](https://www.youtube.com/watch?v=6kmeQj6iW5k)?
|
||||
|
||||
Or maybe a [comprehensive reference](http://mesecons.net/items.html) is your style?
|
||||
|
||||
An overview for the very newest of new beginners? How does [this one](http://uberi.mesecons.net/projects/MeseconsBasics/index.html) look?
|
||||
|
||||
Want to get more into building? Why not check out the [Mesecons Laboratory](http://uberi.mesecons.net/), a website dedicated to advanced Mesecons builders?
|
||||
|
||||
Want to contribute to Mesecons itself? Check out the [source code](https://github.com/minetest-mods/mesecons)!
|
||||
|
||||
Who wrote it anyways?
|
||||
---------------------
|
||||
These awesome people made Mesecons possible!
|
||||
|
||||
| Contributor | Contribution |
|
||||
| --------------- | -------------------------------- |
|
||||
| Hawk777 | Code for VoxelManip caching |
|
||||
| Jat15 | Various tweaks. |
|
||||
| Jeija | **Main developer! Everything.** |
|
||||
| Jordach | Noteblock sounds. |
|
||||
| khonkhortistan | Code, recipes, textures. |
|
||||
| Kotolegokot | Nodeboxes for items. |
|
||||
| minerd247 | Textures. |
|
||||
| Nore/Novatux | Code. |
|
||||
| RealBadAngel | Fixes, improvements. |
|
||||
| sfan5 | Code, recipes, textures. |
|
||||
| suzenako | Piston sounds. |
|
||||
| Uberi/Temperest | Code, textures, documentation. |
|
||||
| VanessaE | Code, recipes, textures, design. |
|
||||
| Whiskers75 | Logic gates implementation. |
|
||||
|
||||
There are also a whole bunch of other people helping with everything from code to testing and feedback. Mesecons would also not be possible without their help!
|
||||
|
||||
Alright, how can I use it?
|
||||
--------------------------
|
||||
All textures in this project are licensed under the CC-BY-SA 3.0 (Creative Commons Attribution-ShareAlike 3.0 Generic). That means you can distribute and remix them as much as you want to, under the condition that you give credit to the authors and the project, and that if you remix and release them, they must be under the same or similar license to this one.
|
||||
|
||||
All code in this project is licensed under the LGPL version 3 or later. That means you have unlimited freedom to distribute and modify the work however you see fit, provided that if you decide to distribute it or any modified versions of it, you must also use the same license. The LGPL also grants the additional freedom to write extensions for the software and distribute them without the extensions being subject to the terms of the LGPL, although the software itself retains its license.
|
||||
|
||||
No warranty is provided, express or implied, for any part of the project.
|
||||
|
||||
"""
|
||||
|
||||
session.add(mod1)
|
||||
session.add(mod2)
|
||||
|
||||
mod = Package()
|
||||
mod.state = PackageState.APPROVED
|
||||
mod.name = "handholds"
|
||||
mod.title = "Handholds"
|
||||
mod.license = licenses["MIT"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ez
|
||||
mod.tags.append(tags["player_effects"])
|
||||
mod.repo = "https://github.com/ezhh/handholds"
|
||||
mod.issueTracker = "https://github.com/ezhh/handholds/issues"
|
||||
mod.forums = 17069
|
||||
mod.short_desc = "Adds hand holds and climbing thingies"
|
||||
mod.desc = "This is the long desc"
|
||||
session.add(mod)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod
|
||||
rel.title = "v1.0.0"
|
||||
rel.max_rel = v4
|
||||
rel.url = "https://github.com/ezhh/handholds/archive/master.zip"
|
||||
rel.approved = True
|
||||
session.add(rel)
|
||||
|
||||
mod = Package()
|
||||
mod.state = PackageState.APPROVED
|
||||
mod.name = "other_worlds"
|
||||
mod.title = "Other Worlds"
|
||||
mod.license = licenses["MIT"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = ez
|
||||
mod.tags.append(tags["mapgen"])
|
||||
mod.tags.append(tags["environment"])
|
||||
mod.repo = "https://github.com/ezhh/other_worlds"
|
||||
mod.issueTracker = "https://github.com/ezhh/other_worlds/issues"
|
||||
mod.forums = 16015
|
||||
mod.short_desc = "Adds space with asteroids and comets"
|
||||
mod.desc = "This is the long desc"
|
||||
session.add(mod)
|
||||
|
||||
mod = Package()
|
||||
mod.state = PackageState.APPROVED
|
||||
mod.name = "food"
|
||||
mod.title = "Food"
|
||||
mod.license = licenses["LGPLv2.1"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = admin_user
|
||||
mod.tags.append(tags["player_effects"])
|
||||
mod.repo = "https://github.com/rubenwardy/food/"
|
||||
mod.issueTracker = "https://github.com/rubenwardy/food/issues/"
|
||||
mod.forums = 2960
|
||||
mod.short_desc = "Adds lots of food and an API to manage ingredients"
|
||||
mod.desc = "This is the long desc"
|
||||
session.add(mod)
|
||||
|
||||
mod = Package()
|
||||
mod.state = PackageState.APPROVED
|
||||
mod.name = "food_sweet"
|
||||
mod.title = "Sweet Foods"
|
||||
mod.license = licenses["CC0"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.MOD
|
||||
mod.author = admin_user
|
||||
mod.tags.append(tags["player_effects"])
|
||||
mod.repo = "https://github.com/rubenwardy/food_sweet/"
|
||||
mod.issueTracker = "https://github.com/rubenwardy/food_sweet/issues/"
|
||||
mod.forums = 9039
|
||||
mod.short_desc = "Adds sweet food"
|
||||
mod.desc = "This is the long desc"
|
||||
food_sweet = mod
|
||||
session.add(mod)
|
||||
|
||||
game1 = Package()
|
||||
game1.state = PackageState.APPROVED
|
||||
game1.name = "capturetheflag"
|
||||
game1.title = "Capture The Flag"
|
||||
game1.type = PackageType.GAME
|
||||
game1.license = licenses["LGPLv2.1"]
|
||||
game1.media_license = licenses["MIT"]
|
||||
game1.author = admin_user
|
||||
game1.tags.append(tags["pvp"])
|
||||
game1.tags.append(tags["survival"])
|
||||
game1.tags.append(tags["multiplayer"])
|
||||
game1.repo = "https://github.com/rubenwardy/capturetheflag"
|
||||
game1.issueTracker = "https://github.com/rubenwardy/capturetheflag/issues"
|
||||
game1.forums = 12835
|
||||
game1.short_desc = "Two teams battle to snatch and return the enemy's flag, before the enemy takes their own!"
|
||||
game1.desc = """
|
||||
As seen on the Capture the Flag server (minetest.rubenwardy.com:30000)
|
||||
|
||||
` `[`javascript:/*--></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/`](javascript:/*--%3E%3C/title%3E%3C/style%3E%3C/textarea%3E%3C/script%3E%3C/xmp%3E%3Csvg/onload='+/%22/+/onmouseover=1/+/)`[*/[]/+alert(1)//'>`
|
||||
|
||||
<IMG SRC="javascript:alert('XSS');">
|
||||
|
||||
<IMG SRC=javascript:alert(&quot;XSS&quot;)>
|
||||
|
||||
``<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>``
|
||||
|
||||
\<a onmouseover="alert(document.cookie)"\>xxs link\</a\>
|
||||
|
||||
\<a onmouseover=alert(document.cookie)\>xxs link\</a\>
|
||||
|
||||
<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>
|
||||
|
||||
<script>alert("hello");</script>
|
||||
|
||||
<SCRIPT SRC=`[`http://xss.rocks/xss.js></SCRIPT>`](http://xss.rocks/xss.js%3E%3C/SCRIPT%3E)`;`
|
||||
|
||||
`<IMG \"\"\">`
|
||||
|
||||
<SCRIPT>
|
||||
|
||||
alert("XSS")
|
||||
|
||||
</SCRIPT>
|
||||
|
||||
<IMG SRC= onmouseover="alert('xxs')">
|
||||
|
||||
<img src=x onerror="javascript:alert('XSS')">
|
||||
|
||||
"\>
|
||||
|
||||
Uses the CTF PvP Engine.
|
||||
"""
|
||||
|
||||
session.add(game1)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = game1
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "https://github.com/rubenwardy/capturetheflag/archive/master.zip"
|
||||
rel.approved = True
|
||||
session.add(rel)
|
||||
|
||||
|
||||
mod = Package()
|
||||
mod.state = PackageState.APPROVED
|
||||
mod.name = "pixelbox"
|
||||
mod.title = "PixelBOX Reloaded"
|
||||
mod.license = licenses["CC0"]
|
||||
mod.media_license = licenses["MIT"]
|
||||
mod.type = PackageType.TXP
|
||||
mod.author = admin_user
|
||||
mod.forums = 14132
|
||||
mod.short_desc = "This is an update of the original PixelBOX texture pack by the brillant artist Gambit"
|
||||
mod.desc = "This is the long desc"
|
||||
session.add(mod)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = mod
|
||||
rel.title = "v1.0.0"
|
||||
rel.url = "http://mamadou3.free.fr/Minetest/PixelBOX.zip"
|
||||
rel.approved = True
|
||||
session.add(rel)
|
||||
|
||||
session.commit()
|
||||
|
||||
metas = {}
|
||||
for package in Package.query.filter_by(type=PackageType.MOD).all():
|
||||
meta = None
|
||||
try:
|
||||
meta = metas[package.name]
|
||||
except KeyError:
|
||||
meta = MetaPackage(package.name)
|
||||
session.add(meta)
|
||||
metas[package.name] = meta
|
||||
package.provides.append(meta)
|
||||
|
||||
dep = Dependency(food_sweet, meta=metas["food"])
|
||||
session.add(dep)
|
||||
@@ -1,6 +1,27 @@
|
||||
title: Help
|
||||
toc: False
|
||||
|
||||
## General Help
|
||||
|
||||
* [Package Tags](package_tags)
|
||||
* [Ranks and Permissions](ranks_permissions)
|
||||
* [Content Ratings and Flags](content_flags)
|
||||
* [Non-free Licenses](non_free)
|
||||
* [Why WTFPL is a terrible license](wtfpl)
|
||||
* [Ranks and Permissions](ranks_permissions)
|
||||
* [Reporting Content](reporting)
|
||||
* [Top Packages Algorithm](top_packages)
|
||||
|
||||
## Help for Package Authors
|
||||
|
||||
* [Package Inclusion Policy and Guidance](/policy_and_guidance/)
|
||||
* [Git Update Detection](update_config)
|
||||
* [Creating Releases using Webhooks](release_webhooks)
|
||||
* [Package Configuration and Releases Guide](package_config)
|
||||
|
||||
## Help for Specific User Ranks
|
||||
|
||||
* [Editors](editors)
|
||||
|
||||
## APIs
|
||||
|
||||
* [API](api)
|
||||
* [Prometheus Metrics](metrics)
|
||||
|
||||
230
app/flatpages/help/api.md
Normal file
230
app/flatpages/help/api.md
Normal file
@@ -0,0 +1,230 @@
|
||||
title: API
|
||||
|
||||
## Responses and Error Handling
|
||||
|
||||
If there is an error, the response will be JSON similar to the following with a non-200 status code:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "The error message"
|
||||
}
|
||||
```
|
||||
|
||||
Successful GET requests will return the resource's information directly as a JSON response.
|
||||
|
||||
Other successful results will return a dictionary with `success` equaling true, and
|
||||
often other keys with information.
|
||||
|
||||
|
||||
## Authentication
|
||||
|
||||
Not all endpoints require authentication, but it is done using Bearer tokens:
|
||||
|
||||
```bash
|
||||
curl https://content.minetest.net/api/whoami/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
```
|
||||
|
||||
Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
|
||||
|
||||
* GET `/api/whoami/`: JSON dictionary with the following keys:
|
||||
* `is_authenticated`: True on successful API authentication
|
||||
* `username`: Username of the user authenticated as, null otherwise.
|
||||
* 4xx status codes will be thrown on unsupported authentication type, invalid access token, or other errors.
|
||||
|
||||
|
||||
## Packages
|
||||
|
||||
* GET `/api/packages/` (List)
|
||||
* See [Package Queries](#package-queries)
|
||||
* GET `/api/packages/<username>/<name>/` (Read)
|
||||
* PUT `/api/packages/<author>/<name>/` (Update)
|
||||
* Requires authentication.
|
||||
* JSON dictionary with any of these keys (all are optional, null to delete Nullables):
|
||||
* `type`: One of `GAME`, `MOD`, `TXP`.
|
||||
* `title`: Human-readable title.
|
||||
* `name`: Technical name (needs permission if already approved).
|
||||
* `short_description`
|
||||
* `tags`: List of tag names, see [misc](#misc).
|
||||
* `content_warnings`: List of content warning names, see [misc](#misc).
|
||||
* `license`: A license name.
|
||||
* `media_license`: A license name.
|
||||
* `long_description`: Long markdown description.
|
||||
* `repo`: Git repo URL.
|
||||
* `website`: Website URL.
|
||||
* `issue_tracker`: Issue tracker URL.
|
||||
* `forums`: forum topic ID.
|
||||
* GET `/api/packages/<username>/<name>/dependencies/`
|
||||
* If query argument `only_hard` is present, only hard deps will be returned.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Edit packages
|
||||
curl -X PUT http://localhost:5123/api/packages/username/name/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "MIT" }'
|
||||
|
||||
# Remove website URL
|
||||
curl -X PUT http://localhost:5123/api/packages/username/name/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d '{ "website": null }'
|
||||
```
|
||||
|
||||
### Package Queries
|
||||
|
||||
Example:
|
||||
|
||||
/api/packages/?type=mod&type=game&q=mobs+fun&hide=nonfree&hide=gore
|
||||
|
||||
Supported query parameters:
|
||||
|
||||
* `type`: Package types (`mod`, `game`, `txp`).
|
||||
* `q`: Query string.
|
||||
* `author`: Filter by author.
|
||||
* `tag`: Filter by tags.
|
||||
* `random`: When present, enable random ordering and ignore `sort`.
|
||||
* `limit`: Return at most `limit` packages.
|
||||
* `hide`: Hide content based on [Content Flags](/help/content_flags/).
|
||||
* `sort`: Sort by (`name`, `title`, `score`, `reviews`, `downloads`, `created_at`, `approved_at`, `last_release`).
|
||||
* `order`: Sort ascending (`asc`) or descending (`desc`).
|
||||
* `protocol_version`: Only show packages supported by this Minetest protocol version.
|
||||
* `engine_version`: Only show packages supported by this Minetest engine version, eg: `5.3.0`.
|
||||
* `fmt`: How the response is formated.
|
||||
* `keys`: author/name only.
|
||||
* `short`: stuff needed for the Minetest client.
|
||||
|
||||
|
||||
## Releases
|
||||
|
||||
* GET `/api/packages/<username>/<name>/releases/` (List)
|
||||
* Returns array of release dictionaries with keys:
|
||||
* `id`: release ID
|
||||
* `title`: human-readable title
|
||||
* `release_date`: Date released
|
||||
* `url`: download URL
|
||||
* `commit`: commit hash or null
|
||||
* `downloads`: number of downloads
|
||||
* `min_minetest_version`: dict or null, minimum supported minetest version (inclusive).
|
||||
* `max_minetest_version`: dict or null, minimum supported minetest version (inclusive).
|
||||
* GET `/api/packages/<username>/<name>/releases/<id>/` (Read)
|
||||
* POST `/api/packages/<username>/<name>/releases/new/` (Create)
|
||||
* Requires authentication.
|
||||
* Body can be JSON or multipart form data. Zip uploads must be multipart form data.
|
||||
* `title`: human-readable name of the release.
|
||||
* For Git release creation:
|
||||
* `method`: must be `git`.
|
||||
* `ref`: (Optional) git reference, eg: `master`.
|
||||
* For zip upload release creation:
|
||||
* `file`: multipart file to upload, like `<input type=file>`.
|
||||
* You can set min and max Minetest Versions [using the content's .conf file](/help/package_config/).
|
||||
* DELETE `/api/packages/<username>/<name>/releases/<id>/` (Delete)
|
||||
* Requires authentication.
|
||||
* Deletes release.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Create release from Git
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d '{ "method": "git", "title": "My Release", "ref": "master" }'
|
||||
|
||||
# Create release from zip upload
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/file.zip
|
||||
|
||||
# Delete release
|
||||
curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/3/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
```
|
||||
|
||||
|
||||
## Screenshots
|
||||
|
||||
* GET `/api/packages/<username>/<name>/screenshots/` (List)
|
||||
* Returns array of screenshot dictionaries with keys:
|
||||
* `id`: screenshot ID
|
||||
* `approved`: true if approved and visible.
|
||||
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
|
||||
* `url`: absolute URL to screenshot.
|
||||
* `created_at`: ISO time.
|
||||
* `order`: Number used in ordering.
|
||||
* GET `/api/packages/<username>/<name>/screenshots/<id>/` (Read)
|
||||
* Returns screenshot dictionary like above.
|
||||
* POST `/api/packages/<username>/<name>/screenshots/new/` (Create)
|
||||
* Requires authentication.
|
||||
* Body is multipart form data.
|
||||
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
|
||||
* `file`: multipart file to upload, like `<input type=file>`.
|
||||
* DELETE `/api/packages/<username>/<name>/screenshots/<id>/` (Delete)
|
||||
* Requires authentication.
|
||||
* Deletes screenshot.
|
||||
* POST `/api/packages/<username>/<name>/screenshots/order/`
|
||||
* Requires authentication.
|
||||
* Body is a JSON array containing the screenshot IDs in their order.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# Create screenshots
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" \
|
||||
-F title="My Release" -F file=@path/to/screnshot.png
|
||||
|
||||
# Delete screenshot
|
||||
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
|
||||
-H "Authorization: Bearer YOURTOKEN"
|
||||
|
||||
# Reorder screenshots
|
||||
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/order/ \
|
||||
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
|
||||
-d "[13, 2, 5, 7]"
|
||||
```
|
||||
|
||||
|
||||
## Topics
|
||||
|
||||
* GET `/api/topics/`: Supports [Package Queries](#package-queries), and the following two options:
|
||||
* `show_added`: Show topics which exist as packages, default true.
|
||||
* `show_discarded`: Show topics which have been marked as outdated, default false.
|
||||
|
||||
### Topic Queries
|
||||
|
||||
Example:
|
||||
|
||||
/api/topics/?q=mobs
|
||||
|
||||
Supported query parameters:
|
||||
|
||||
* `q`: Query string.
|
||||
* `sort`: Sort by (`name`, `views`, `date`).
|
||||
* `order`: Sort ascending (`asc`) or descending (`desc`).
|
||||
* `show_added`: Show topics that have an existing package.
|
||||
* `show_discarded`: Show topics marked as discarded.
|
||||
* `limit`: Return at most `limit` topics.
|
||||
|
||||
|
||||
## Misc
|
||||
|
||||
* GET `/api/scores/`
|
||||
* See [Package Queries](#package-queries)
|
||||
* GET `/api/tags/`: List of:
|
||||
* `name`: technical name
|
||||
* `title`: human-readable title
|
||||
* `description`: tag description or null
|
||||
* GET `/api/licenses/`: List of:
|
||||
* `name`
|
||||
* `is_foss`: whether the license is foss
|
||||
* GET `/api/homepage/`
|
||||
* `count`: number of packages
|
||||
* `downloads`: get number of downloads
|
||||
* `new`: new packages
|
||||
* `updated`: recently updated packages
|
||||
* `pop_mod`: popular mods
|
||||
* `pop_txp`: popular textures
|
||||
* `pop_game`: popular games
|
||||
* `high_reviewed`: highest reviewed
|
||||
* GET `/api/minetest_versions/`
|
||||
@@ -6,21 +6,29 @@ your client to use new flags.
|
||||
|
||||
## Flags
|
||||
|
||||
Minetest allows you to specify a comma-separated list of flags to hide in the
|
||||
client:
|
||||
|
||||
```
|
||||
contentdb_flag_blacklist = nonfree, bad_language, drugs
|
||||
```
|
||||
|
||||
A flag can be:
|
||||
|
||||
* `nonfree` - can be used to hide packages which do not qualify as
|
||||
'free software', as defined by the Free Software Foundation.
|
||||
* A content rating, given below.
|
||||
* A content warning, given below.
|
||||
* `android_default` - meta-flag that filters out any content with a content warning.
|
||||
* `desktop_default` - meta-flag that doesn't filter anything out for now.
|
||||
|
||||
## Content Warnings
|
||||
|
||||
## Ratings
|
||||
Packages with mature content will be tagged with a content warning based
|
||||
on the content type.
|
||||
|
||||
Content ratings aren't currently supported by ContentDB.
|
||||
Instead, mature content isn't allowed at all for now.
|
||||
|
||||
In the future, more mature content will be allowed but labelled with
|
||||
content ratings which may contain the following:
|
||||
|
||||
* android_default - meta-rating which includes gore and drugs.
|
||||
* desktop_default - meta-rating which won't include anything for now.
|
||||
* gore - more than just blood
|
||||
* drugs
|
||||
* swearing
|
||||
* `bad_language` - swearing.
|
||||
* `drugs` - drugs or alcohol.
|
||||
* `gambling`
|
||||
* `gore` - blood, etc.
|
||||
* `horror` - shocking and scary content.
|
||||
* `violence` - non-cartoon violence.
|
||||
|
||||
34
app/flatpages/help/editors.md
Normal file
34
app/flatpages/help/editors.md
Normal file
@@ -0,0 +1,34 @@
|
||||
title: Editors
|
||||
|
||||
## What should editors do?
|
||||
|
||||
Editors are users of rank Editor or above.
|
||||
They are responsible for ensuring that the package listings of ContentDB are useful.
|
||||
For this purpose, they can/will:
|
||||
|
||||
* Review and approve packages.
|
||||
* Edit any package - including tags, releases, screenshots, and maintainers.
|
||||
* Create packages on behalf of authors who aren't present.
|
||||
|
||||
Editors should make sure they are familiar with the
|
||||
[Package Inclusion Policy and Guidance](/policy_and_guidance/).
|
||||
|
||||
## ContentDB is not a curated platform
|
||||
|
||||
It's important to note that ContentDB isn't a curated platform, but it also does have some
|
||||
requirements on minimum usefulness. See 2.2 in the [Policy and Guidance](/policy_and_guidance/).
|
||||
|
||||
## Editor Work Queue
|
||||
|
||||
The [Editor Work Queue](/todo/) and related pages contain useful information for editors, such as:
|
||||
|
||||
* The package, release, and screenshot approval queues.
|
||||
* Packages which are outdated or are missing tags.
|
||||
* A list of forum topics without packages.
|
||||
Editors can create the packages or "discard" them if they don't think it's worth adding them.
|
||||
|
||||
## Editor Notifications
|
||||
|
||||
Editors currently receive notifications for any new thread opened on a package, so that they
|
||||
know when a user is asking for help. These notifications are shown separately in the notifications
|
||||
interface, and can be configured separately in Emails and Notifications.
|
||||
16
app/flatpages/help/metrics.md
Normal file
16
app/flatpages/help/metrics.md
Normal file
@@ -0,0 +1,16 @@
|
||||
title: Prometheus Metrics
|
||||
|
||||
## What is Prometheus?
|
||||
|
||||
[Prometheus](https://prometheus.io) is an "open-source monitoring system with a
|
||||
dimensional data model, flexible query language, efficient time series database
|
||||
and modern alerting approach".
|
||||
|
||||
Prometheus Metrics can be accessed at [/metrics](/metrics).
|
||||
|
||||
## Metrics
|
||||
|
||||
* `contentdb_packages` - Total packages (counter).
|
||||
* `contentdb_users` - Number of registered users (counter).
|
||||
* `contentdb_downloads` - Total downloads (counter).
|
||||
* `contentdb_score` - Total package score (gauge).
|
||||
73
app/flatpages/help/non_free.md
Normal file
73
app/flatpages/help/non_free.md
Normal file
@@ -0,0 +1,73 @@
|
||||
title: Non-free Licenses
|
||||
|
||||
## What are Non-Free, Free, and Open Source licenses?
|
||||
|
||||
A non-free license is one that does not meet the
|
||||
[Free Software Definition](https://www.gnu.org/philosophy/free-sw.en.html)
|
||||
or the [Open Source Definition](https://opensource.org/osd).
|
||||
ContentDB will clearly label any packages with non-free licenses,
|
||||
and they will be subject to limited promotion.
|
||||
|
||||
## How does ContentDB deal with Non-Free Licenses?
|
||||
|
||||
**ContentDB does not allow certain non-free licenses, and will limit the promotion
|
||||
of packages with non-free licenses.**
|
||||
|
||||
Minetest is free and open source software, and is only as big as it is now
|
||||
because of this. It's pretty amazing you can take nearly any published mod and modify it
|
||||
to how you like - add some features, maybe fix some bugs - and then share those
|
||||
modifications without the worry of legal issues. The project, itself, relies on open
|
||||
source contributions to survive - if it were non-free, then it would have died
|
||||
when celeron55 lost interest.
|
||||
|
||||
If you have played nearly any game with a large modding scene, you will find
|
||||
that most mods are legally ambiguous. A lot of them don't even provide the
|
||||
source code to allow you to bug fix or extend as you need.
|
||||
|
||||
Limiting the promotion of problematic licenses helps Minetest avoid ending up in
|
||||
such a state. Licenses that prohibit redistribution or modification are
|
||||
completely banned from ContentDB and the Minetest forums. Other non-free licenses
|
||||
will be subject to limited promotion - they won't be shown by default in
|
||||
the client.
|
||||
|
||||
Not providing full promotion on ContentDB, or not allowing your package at all,
|
||||
doesn't mean you can't make such content - it just means we're not going to help
|
||||
you spread it.
|
||||
|
||||
## What's so bad about licenses that forbid commercial use?
|
||||
|
||||
Please read [reasons not to use a Creative Commons -NC license](https://freedomdefined.org/Licenses/NC).
|
||||
Here's a quick summary related to Minetest content:
|
||||
|
||||
1. They make your work incompatible with a growing body of free content, even if
|
||||
you do want to allow derivative works or combinations.
|
||||
This means that it can cause problems when another modder wishes to include your
|
||||
work in a modpack or game.
|
||||
2. They may rule out other basic and beneficial uses that you want to allow.
|
||||
For example, CC -NC will forbid showing your content in a monetised YouTube
|
||||
video.
|
||||
3. They are unlikely to increase the potential profit from your work, and a
|
||||
share-alike license serves the goal to protect your work from unethical
|
||||
exploitation equally well.
|
||||
|
||||
## How can I show non-free packages in the client?
|
||||
|
||||
Non-free packages are hidden in the client by default, partly in order to comply
|
||||
with the rules of various Linux distributions.
|
||||
|
||||
Users can opt-in to showing non-free software, if they wish:
|
||||
|
||||
1. In the main menu, go to Settings > All settings
|
||||
2. Search for "ContentDB Flag Blacklist".
|
||||
3. Edit that setting to remove `nonfree, `.
|
||||
|
||||
<figure class="figure my-4">
|
||||
<img class="figure-img img-fluid rounded" src="/static/contentdb_flag_blacklist.png" alt="Screenshot of the ContentDB Flag Blacklist setting">
|
||||
<figcaption class="figure-caption">Screenshot of the ContentDB Flag Blacklist setting</figcaption>
|
||||
</figure>
|
||||
|
||||
In the future, [the `platform_default` flag](/help/content_flags/) will be used to control what content
|
||||
each platforms shows - Android is significantly stricter about mature content.
|
||||
You may wish to remove all text from that setting completely, leaving it blank,
|
||||
if you wish to view all content when this happens. Currently, [mature content is
|
||||
not permitted on ContentDB](/policy_and_guidance/).
|
||||
125
app/flatpages/help/package_config.md
Normal file
125
app/flatpages/help/package_config.md
Normal file
@@ -0,0 +1,125 @@
|
||||
title: Package Configuration and Releases Guide
|
||||
|
||||
## Introduction
|
||||
|
||||
ContentDB will read configuration files in your package when doing several
|
||||
tasks, including package and release creation. This page details how you can use
|
||||
this to your advantage.
|
||||
|
||||
## .conf files
|
||||
|
||||
### What is a content .conf file?
|
||||
|
||||
Every type of content can have a `.conf` file that contains the metadata.
|
||||
|
||||
The filename of the `.conf` file depends on the content type:
|
||||
|
||||
* `mod.conf` for mods.
|
||||
* `modpack.conf` for mod packs.
|
||||
* `game.conf` for games.
|
||||
* `texture_pack.conf` for texture packs.
|
||||
|
||||
The `.conf` uses a key-value format, separated using equals. Here's a simple example:
|
||||
|
||||
name = mymod
|
||||
description = A short description to show in the client.
|
||||
|
||||
### Understood values
|
||||
|
||||
ContentDB understands the following information:
|
||||
|
||||
* `description` - A short description to show in the client.
|
||||
* `depends` - Comma-separated hard dependencies.
|
||||
* `optional_depends` - Comma-separated soft dependencies.
|
||||
* `min_minetest_version` - The minimum Minetest version this runs on, see [Min and Max Minetest Versions](#min_max_versions).
|
||||
* `max_minetest_version` - The maximum Minetest version this runs on, see [Min and Max Minetest Versions](#min_max_versions).
|
||||
|
||||
and for mods only:
|
||||
|
||||
* `name` - the mod technical name.
|
||||
|
||||
|
||||
## .cdb.json
|
||||
|
||||
You can include a `.cdb.json` file in the root of your content directory (ie: next to a .conf)
|
||||
to update the package meta.
|
||||
|
||||
It should be a JSON dictionary with one or more of the following optional keys:
|
||||
|
||||
* `type`: One of `GAME`, `MOD`, `TXP`.
|
||||
* `title`: Human-readable title.
|
||||
* `name`: Technical name (needs permission if already approved).
|
||||
* `short_description`
|
||||
* `tags`: List of tag names, see [/api/tags/](/api/tags/).
|
||||
* `content_warnings`: List of content warning names, see [/api/content_warnings/](/api/content_warnings/).
|
||||
* `license`: A license name, see [/api/licenses/](/api/licenses/).
|
||||
* `media_license`: A license name.
|
||||
* `long_description`: Long markdown description.
|
||||
* `repo`: Git repo URL.
|
||||
* `website`: Website URL.
|
||||
* `issue_tracker`: Issue tracker URL.
|
||||
* `forums`: forum topic ID.
|
||||
|
||||
Use `null` to unset fields where relevant.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Foo bar",
|
||||
"tags": ["pvp", "survival"],
|
||||
"license": "MIT",
|
||||
"website": null
|
||||
}
|
||||
```
|
||||
|
||||
## Controlling Release Creation
|
||||
|
||||
### Git-based Releases and Submodules
|
||||
|
||||
ContentDB can create releases from a Git repository.
|
||||
It will include submodules in the resulting archive.
|
||||
Simply set VCS Repository in the package's meta to a Git repository, and then
|
||||
choose Git as the method when creating a release.
|
||||
|
||||
### Automatic Release Creation
|
||||
|
||||
See [Git Update Detection](/help/update_config/).
|
||||
You can also use [GitLab/GitHub webhooks](/help/release_webhooks/) or the [API](/help/api/)
|
||||
to create releases.
|
||||
|
||||
### Min and Max Minetest Versions
|
||||
|
||||
<a name="min_max_versions" />
|
||||
|
||||
When creating a release, the `.conf` file will be read to determine what Minetest
|
||||
versions the release supports. If the `.conf` doesn't specify, then it is assumed
|
||||
that it supports all versions.
|
||||
|
||||
This happens when you create a release via the ContentDB web interface, the
|
||||
[API](/help/api/), or using a [GitLab/GitHub webhook](/help/release_webhooks/).
|
||||
|
||||
Here's an example config:
|
||||
|
||||
name = mymod
|
||||
min_minetest_version = 5.0
|
||||
max_minetest_version = 5.3
|
||||
|
||||
Leaving out min or max to have them set as "None".
|
||||
|
||||
### Excluding files
|
||||
|
||||
When using Git to create releases,
|
||||
you can exclude files from a release by using [gitattributes](https://git-scm.com/docs/gitattributes):
|
||||
|
||||
|
||||
.* export-ignore
|
||||
sources export-ignore
|
||||
*.zip export-ignore
|
||||
|
||||
|
||||
This will prevent any files from being included if they:
|
||||
|
||||
* Beginning with `.`
|
||||
* or are named `sources` or are inside any directory named `sources`.
|
||||
* or have an extension of "zip".
|
||||
@@ -1,33 +0,0 @@
|
||||
title: Package Tags
|
||||
|
||||
## Overview
|
||||
|
||||
Tags should be added to packages to enable easy identification of different types of mods, games and texture packs.
|
||||
|
||||
They are only beneficial when applied correctly, so please use the following guidelines.
|
||||
|
||||
## Tag Usage
|
||||
|
||||
* **Building** - For mods that focus on adding new materials or nodes to build with.
|
||||
* **Education** - For mods or games created for educational purposes.
|
||||
* **Environment** - For mods that add environmental effects, including ambient sound and weather effects.
|
||||
* **Inventory** - For mods that add new inventory systems or new inventory pages.
|
||||
* **Machines and Electronics** - For mods that include placeable machinery or electronic components which interact to complete tasks.
|
||||
* **Maintenance** - For mods that assist with world or player maintenance. This includes large-scale map manipulation, area protection and other administrative tools.
|
||||
* **Mapgen** - For mods that add new biomes, new mapgen decorations, or any other mapgen elements.
|
||||
* **Mobs and NPCs** - For mods that add mobs or NPCs, or provide tools that assist with mob and NPC creation or manipulation.
|
||||
* **Plants and Farming** - For mods that add new plants or other farmable resources.
|
||||
* **Player effects/Food** - For mods that change player effects, for example speed, jump height or gravity, and food.
|
||||
* **Tools** - For mods that add new tools or new features for existing tools.
|
||||
* **Transport** - For mods that add transportation methods. This includes teleportation, vehicles and ridable mobs.
|
||||
* **Survival** - For mods written specifically for survival games. For example, these mods might focus on game-balance or increase the difficulty level. This tag should also be used for games with a heavy survival focus.
|
||||
* **Creative** - For mods written specifically (and often exclusively) for use in creative mode. For example, these mods may add a large amount of decorative content, or content without crafting recipes. This tag should also be used for games with a heavy creative focus.
|
||||
* **Multiplayer-focused** - For games that can be played with other players.
|
||||
* **Singleplayer-focused** - For games that can be played alone.
|
||||
* **PvP** - For games designed to be played competitively against other players.
|
||||
* **PvE** - For games designed for one or multiple players which focus on combat against mobs or NPCs.
|
||||
* **Puzzle** - For mods and games with a focus on puzzle solving instead of combat.
|
||||
* **16px** - For 16px texture packs.
|
||||
* **32px** - For 32px texture packs.
|
||||
* **64px** - For 64px texture packs.
|
||||
* **128px+** - For 128px or higher texture packs.
|
||||
@@ -3,24 +3,24 @@ title: Ranks and Permissions
|
||||
## Overview
|
||||
|
||||
* **New Members** - mostly untrusted, cannot change package meta data or publish releases without approval.
|
||||
* **Members** - Trusted to change the meta data of their own packages', but cannot publish releases.
|
||||
* **Trusted Members** - Same as above, but can approve their own releases and packages.
|
||||
* **Editors** - Trusted to change the meta data of any package, and also make and publish releases.
|
||||
* **Members** - Trusted to change the meta data of their own packages', but cannot approve their own packages.
|
||||
* **Trusted Members** - Same as above, but can approve their own releases.
|
||||
* **Editors** - Trusted to edit any package or release, and also responsible for approving new packages.
|
||||
* **Moderators** - Same as above, but can manage users.
|
||||
* **Admins** - Full access.
|
||||
|
||||
## Breakdown
|
||||
|
||||
<table class="fancyTable">
|
||||
<table class="table table-striped ranks-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th colspan=2>New Member</th>
|
||||
<th colspan=2>Member</th>
|
||||
<th colspan=2>Trusted Member</th>
|
||||
<th colspan=2>Editor</th>
|
||||
<th colspan=2>Moderator</th>
|
||||
<th colspan=2>Admin</th>
|
||||
<th colspan=2 class="NEW_MEMBER">New Member</th>
|
||||
<th colspan=2 class="MEMBER">Member</th>
|
||||
<th colspan=2 class="TRUSTED_MEMBER">Trusted</th>
|
||||
<th colspan=2 class="EDITOR">Editor</th>
|
||||
<th colspan=2 class="MODERATOR">Moderator</th>
|
||||
<th colspan=2 class="ADMIN">Admin</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Owner of thing</th>
|
||||
@@ -41,203 +41,232 @@ title: Ranks and Permissions
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Create Package</td>
|
||||
<th>✓</th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td>✓</td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Approve Package</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th></th> <!-- member -->
|
||||
<th></th>
|
||||
<th></th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td></td> <!-- member -->
|
||||
<td></td>
|
||||
<td></td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Delete Package</td>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Edit Package</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Edit Maintainers</td>
|
||||
<td>✓</td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Add/Delete Screenshot</td>
|
||||
<th>✓</th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td>✓</td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Approve Screenshot</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th></th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Approve EditRequest</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Edit EditRequest</td>
|
||||
<th>✓<sup>1</sup></th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td></td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Make Release</td>
|
||||
<th>✓</th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td>✓</td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Approve Release</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th></th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Change Release URL</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th></th> <!-- member -->
|
||||
<th></th>
|
||||
<th></th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th></th> <!-- editor -->
|
||||
<th></th>
|
||||
<th></th> <!-- moderator -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td></td> <!-- member -->
|
||||
<td></td>
|
||||
<td></td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td></td> <!-- editor -->
|
||||
<td></td>
|
||||
<td></td> <!-- moderator -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>See Private Thread</td>
|
||||
<th>✓</th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<th>✓</th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td>✓</td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td>✓</td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Edit Comments</td>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Set Email</td>
|
||||
<th>✓</th> <!-- new -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- editor -->
|
||||
<th></th>
|
||||
<th>✓</th> <!-- moderator -->
|
||||
<td>✓</td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<th>✓<sup>2</sup></th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create Token</td>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- editor -->
|
||||
<td></td>
|
||||
<td>✓</td> <!-- moderator -->
|
||||
<th>✓<sup>2</sup></th>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Set Rank</td>
|
||||
<th></th> <!-- new -->
|
||||
<th></th>
|
||||
<th></th> <!-- member -->
|
||||
<th></th>
|
||||
<th></th> <!-- trusted member -->
|
||||
<th></th>
|
||||
<th></th> <!-- editor -->
|
||||
<th></th>
|
||||
<td></td> <!-- new -->
|
||||
<td></td>
|
||||
<td></td> <!-- member -->
|
||||
<td></td>
|
||||
<td></td> <!-- trusted member -->
|
||||
<td></td>
|
||||
<td></td> <!-- editor -->
|
||||
<td></td>
|
||||
<th>✓<sup>3</sup></th> <!-- moderator -->
|
||||
<th>✓<sup>2</sup><sup>3</sup></th>
|
||||
<th>✓</th> <!-- admin -->
|
||||
<th>✓</th>
|
||||
<td>✓</td> <!-- admin -->
|
||||
<td>✓</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
1. User must be the author of the EditRequest.
|
||||
2. Target user cannot be an admin.
|
||||
3. Cannot set user to a higher rank than themselves.
|
||||
|
||||
57
app/flatpages/help/release_webhooks.md
Normal file
57
app/flatpages/help/release_webhooks.md
Normal file
@@ -0,0 +1,57 @@
|
||||
title: Creating Releases using Webhooks
|
||||
|
||||
## What does this mean?
|
||||
|
||||
A webhook is a notification from one service to another. Put simply, a webhook
|
||||
is used to notify ContentDB that the git repository has changed.
|
||||
|
||||
ContentDB offers the ability to automatically create releases using webhooks
|
||||
from either Github or Gitlab. If you're not using either of those services,
|
||||
you can also use the [API](../api) to create releases.
|
||||
|
||||
ContentDB also offers the ability to poll a Git repo and check for updates
|
||||
without any web hooks, this is limited to once a day.
|
||||
See [Git Update Detection](/help/update_config/).
|
||||
|
||||
The process is as follows:
|
||||
|
||||
1. The user creates an API Token and a webhook to use it.
|
||||
2. The user pushes a commit to the git host (Gitlab or Github).
|
||||
3. The git host posts a webhook notification to ContentDB, using the API token assigned to it.
|
||||
4. ContentDB checks the API token and issues a new release.
|
||||
|
||||
## Setting up
|
||||
|
||||
### GitHub
|
||||
|
||||
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
|
||||
2. Copy the access token that was generated.
|
||||
3. Go to the GitLab repository's settings > Webhooks > Add Webhook.
|
||||
4. Set the payload URL to `https://content.minetest.net/github/webhook/`
|
||||
5. Set the content type to JSON.
|
||||
6. Set the secret to the access token that you copied.
|
||||
7. Set the events
|
||||
* If you want a rolling release, choose "just the push event".
|
||||
* Or if you want a stable release cycle based on tags,
|
||||
choose "Let me select" > Branch or tag creation.
|
||||
8. Create.
|
||||
|
||||
### GitLab
|
||||
|
||||
1. Create a ContentDB API Token at [Profile > API Tokens: Manage](/user/tokens/).
|
||||
2. Copy the access token that was generated.
|
||||
3. Go to the GitLab repository's settings > Webhooks.
|
||||
4. Set the URL to `https://content.minetest.net/gitlab/webhook/`
|
||||
6. Set the secret token to the ContentDB access token that you copied.
|
||||
7. Set the events
|
||||
* If you want a rolling release, choose "Push events".
|
||||
* Or if you want a stable release cycle based on tags,
|
||||
choose "Tag push events".
|
||||
8. Add webhook.
|
||||
|
||||
## Configuring
|
||||
|
||||
See the [Package Configuration and Releases Guide](/help/package_config/) for
|
||||
documentation on configuring the release creation.
|
||||
You can set the min/max Minetest version from the Git repository, and also
|
||||
configure what files are included.
|
||||
36
app/flatpages/help/top_packages.md
Normal file
36
app/flatpages/help/top_packages.md
Normal file
@@ -0,0 +1,36 @@
|
||||
title: Top Packages Algorithm
|
||||
|
||||
## Package Score
|
||||
|
||||
Each package is given a `score`, which is used when ordering them in the
|
||||
"Top Games/Mods/Texture Packs" lists. The intention of this feature is
|
||||
to make it easier for new users to find good packages.
|
||||
|
||||
A package's score is equal to a rolling average of recent downloads,
|
||||
plus the sum of the score given by reviews.
|
||||
|
||||
A review score is 100 if positive, -100 if negative.
|
||||
|
||||
```c
|
||||
reviews_sum = sum(100 * (positive ? 1 : -1));
|
||||
score = avg_downloads + reviews_sum;
|
||||
```
|
||||
|
||||
## Pseudo rolling average of downloads
|
||||
|
||||
Each package adds 1 to `avg_downloads` for each unique download,
|
||||
and then loses 5% (=1/20) of the value every day.
|
||||
|
||||
This is called a [Frecency](https://en.wikipedia.org/wiki/Frecency) heuristic,
|
||||
a measure which combines both frequency and recency.
|
||||
|
||||
"Unique download" is counted per IP per package.
|
||||
Downloading an update won't increase the download count if it's already been
|
||||
downloaded from that IP.
|
||||
|
||||
## Transparency and Feedback
|
||||
|
||||
You can see all scores using the [scores REST API](/api/scores/), or by
|
||||
using the [Prometheus metrics](/help/metrics/) endpoint.
|
||||
|
||||
Consider [suggesting improvements](https://github.com/minetest/contentdb/issues/new?assignees=&labels=Policy&template=policy.md&title=).
|
||||
43
app/flatpages/help/update_config.md
Normal file
43
app/flatpages/help/update_config.md
Normal file
@@ -0,0 +1,43 @@
|
||||
title: Git Update Detection
|
||||
|
||||
## Introduction
|
||||
|
||||
When you push a change to your Git repository, ContentDB can create a new release automatically or
|
||||
send you a reminder. ContentDB will check your Git repository one per day, but you can use
|
||||
webhooks or the API for faster updates.
|
||||
|
||||
Git Update Detection is clever enough to not create a release again if you've already created
|
||||
it manually or using webhooks/the API.
|
||||
|
||||
## Setting up
|
||||
|
||||
* Set "VCS Repository URL" in your package.
|
||||
* Open the "Configure Git Update Detection" page:
|
||||
* Go to the Create Release page and click "Set up" on the banner.
|
||||
* If the "How do you want to create releases?" wizard appears, choose "Automatic".
|
||||
* Choose a trigger:
|
||||
* **New Commit** - this will trigger for each pushed commit on the default branch, or the branch you specify.
|
||||
* **New Tag** - this will trigger when a New Tag is created.
|
||||
* Choose action to occur when the trigger happens:
|
||||
* **Notification** - All maintainers receive a notification under the Bot category, and the package
|
||||
will appear under "Outdated Packages" in [your to do list](/user/todo/).
|
||||
* **Create Release** - A new release is created.
|
||||
If New Commit, the title will be the iso date (eg: 2021-02-01).
|
||||
If New Tag, the title will the tag name.
|
||||
|
||||
## Marking a package as up-to-date
|
||||
|
||||
Git Update Detection shouldn't erroneously mark packages as outdated if it is configured currently,
|
||||
so the first thing you should do is make sure the Update Settings are set correctly.
|
||||
|
||||
There are some situations where the settings are correct, but you want to mark a package as
|
||||
up-to-date - for example, if you don't want to make a release for a particular tag.
|
||||
Clicking "Save" on "Update Settings" will mark a package as up-to-date.
|
||||
|
||||
## Configuring Release Creation
|
||||
|
||||
See the [Package Configuration and Releases Guide](/help/package_config/) for
|
||||
documentation on configuring the release creation.
|
||||
|
||||
From the Git repository, you can set the min/max Minetest versions, which files are included,
|
||||
and update the package meta.
|
||||
@@ -1,5 +1,5 @@
|
||||
title: WTFPL is a terrible license
|
||||
no_h1: true
|
||||
toc: False
|
||||
|
||||
<div id="warning" class="alert alert-warning">
|
||||
<span class="icon_message"></span>
|
||||
@@ -20,8 +20,6 @@ no_h1: true
|
||||
</script>
|
||||
</div>
|
||||
|
||||
# WTFPL is a terrible license
|
||||
|
||||
The use of WTFPL as a license is discouraged for multiple reasons.
|
||||
|
||||
* **No Warranty disclaimer:** This could open you up to being sued.<sup>[1]</sup>
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
title: Package Inclusion Policy and Guidance
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<b>Note:</b> This is a draft
|
||||
</div>
|
||||
|
||||
## 0. Overview
|
||||
|
||||
ContentDB is for the community, and as such listings should be useful to the
|
||||
community. To help with this, there are a few rules to improve the quality of
|
||||
the listings and to combat abuse.
|
||||
|
||||
* No inappropriate content. <sup>2.1</sup>
|
||||
* Content must be playable/useful, but not necessarily finished. <sup>2.2</sup>
|
||||
* Don't use the name of another mod unless your mod is a fork or reimplementation. <sup>3</sup>
|
||||
* Licenses must allow derivatives, redistribution, and must not discriminate. <sup>4</sup>
|
||||
* Don't put promotions or advertisements in package listings, except for
|
||||
donation and personal website links which are permitted in the
|
||||
long description. <sup>5</sup>
|
||||
* The ContentDB admin reserves the right to remove packages for any reason,
|
||||
* **No inappropriate content.** <sup>2.1</sup>
|
||||
* **Content must be playable/useful, but not necessarily finished.** <sup>2.2</sup>
|
||||
* **Don't use the name of another mod unless your mod is a fork or reimplementation.** <sup>3</sup>
|
||||
* **Licenses must allow derivatives, redistribution, and must not discriminate.** <sup>4</sup>
|
||||
* **Don't put promotions or advertisements in any package metadata.** <sup>5</sup>
|
||||
* **The ContentDB admin reserves the right to remove packages for any reason**,
|
||||
including ones not covered by this document, and to ban users who abuse
|
||||
this service. <sup>1</sup>
|
||||
|
||||
@@ -27,27 +21,26 @@ the listings and to combat abuse.
|
||||
The ContentDB admin reserves the right to remove packages for any reason,
|
||||
including ones not covered by this document, and to ban users who abuse this service.
|
||||
|
||||
Also see the [help page on tags](/help/package_tags/).
|
||||
|
||||
|
||||
## 2. Accepted Content
|
||||
|
||||
### 2.1. Acceptable Content
|
||||
|
||||
Sexually-orientated content is not permitted.
|
||||
If in doubt at what this means, [contact us by raising a report](/help/reporting/).
|
||||
|
||||
Mature content, including that relating to drugs, excessive gore, violence, or
|
||||
excessive horror, is not currently permitted - but will be in the future.
|
||||
Mature content is permitted providing that it is labelled correctly.
|
||||
See [Content Flags](/help/content_flags/).
|
||||
|
||||
The submission of malware is strictly prohibited. This includes software which
|
||||
does not do as it advertises, for example if it posts telemetry without stating
|
||||
The submission of malware is strictly prohibited. This includes software that
|
||||
does not do as it advertises, for example, if it posts telemetry without stating
|
||||
clearly that it does in the package meta.
|
||||
|
||||
### 2.2. State of Completion
|
||||
|
||||
ContentDB should only currently contain playable content - content which is
|
||||
sufficiently complete to be useful to end users. It's fine to add stuff which
|
||||
is still a work in progress (WIP) as long as it adds sufficient value -
|
||||
sufficiently complete to be useful to end-users. It's fine to add stuff which
|
||||
is still a Work in Progress (WIP) as long as it adds sufficient value;
|
||||
MineClone 2 is a good example of a WIP package which may break between releases
|
||||
but still has value. Note that this doesn't mean that you should add a thing
|
||||
you started working on yesterday, it's worth adding all the basic stuff to
|
||||
@@ -62,8 +55,10 @@ libraries allows them to be installed when a mod depends on it.
|
||||
|
||||
### 3.1 Right to a name
|
||||
|
||||
A package uses a name when it has that name or contains a mod that uses that name.
|
||||
|
||||
The first package to use a name based on the creation of its forum topic or
|
||||
contentdb submission has the right to the technical name. The use of a package
|
||||
ContentDB submission has the right to the technical name. The use of a package
|
||||
on a server or in private doesn't reserve its name. No other packages of the same
|
||||
type may use the same name, except for the exception given by 3.2.
|
||||
|
||||
@@ -76,10 +71,10 @@ to change the name of the package, or your package won't be accepted.
|
||||
|
||||
We reserve the right to issue exceptions for this where we feel necessary.
|
||||
|
||||
### 3.2 Mod Forks and Reimplementations
|
||||
### 3.2. Mod Forks and Reimplementations
|
||||
|
||||
An exception to the above is that mods are allowed to have the same name as a
|
||||
mod if its a fork of that mod (or a close reimplementation). In real terms, it
|
||||
mod if it's a fork of that mod (or a close reimplementation). In real terms, it
|
||||
should be possible to use the new mod as a drop-in replacement.
|
||||
|
||||
We reserve the right to decide whether a mod counts as a fork or
|
||||
@@ -88,14 +83,14 @@ reimplementation of the mod that owns the name.
|
||||
|
||||
## 4. Licenses
|
||||
|
||||
### 4.1 Allowed Licenses
|
||||
### 4.1. Allowed Licenses
|
||||
|
||||
Please ensure that you correctly credit any resources (code, assets, or otherwise)
|
||||
that you have used in your package.
|
||||
|
||||
**The use of licenses which do not allow derivatives or redistribution is not
|
||||
**The use of licenses that do not allow derivatives or redistribution is not
|
||||
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.
|
||||
The use of licenses which discriminate between groups of people or forbid the use
|
||||
The use of licenses that discriminate between groups of people or forbid the use
|
||||
of the content on servers or singleplayer is also not permitted.**
|
||||
|
||||
However, closed sourced licenses are allowed if they allow the above.
|
||||
@@ -106,17 +101,21 @@ get around to adding it.
|
||||
Please note that the definitions of "free" and "non-free" is the same as that
|
||||
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
|
||||
|
||||
### 4.2 Recommended Licenses
|
||||
### 4.2. Recommended Licenses
|
||||
|
||||
It is highly recommended that you use a free and open source software license.
|
||||
FOSS licenses result in a sharing community and will increase the number of potential users your package has.
|
||||
Using a closed source license will result in your package being massively penalised in the search results and package lists.
|
||||
It is highly recommended that you use a Free and Open Source software (FOSS)
|
||||
license. FOSS licenses result in a sharing community and will increase the
|
||||
number of potential users your package has. Using a closed source license will
|
||||
result in your package being massively penalised in the search results and
|
||||
package lists. See the help page on [non-free licenses](/help/non_free/) for more
|
||||
information.
|
||||
|
||||
It is recommended that you use a proper license for code with a warranty
|
||||
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
|
||||
for media, such as a Creative Commons license.
|
||||
|
||||
The use of WTFPL is discouraged as it doesn't contain a [valid warranty disclaimer](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html),
|
||||
The use of WTFPL is discouraged as it doesn't contain a
|
||||
[valid warranty disclaimer](https://cubicspot.blogspot.com/2017/04/wtfpl-is-harmful-to-software-developers.html),
|
||||
and also includes swearing which prevents settings like schools from using your content.
|
||||
[Read more](/help/wtfpl/).
|
||||
|
||||
@@ -125,8 +124,8 @@ Public domain is not a valid license in many countries, please use CC0 or MIT in
|
||||
|
||||
## 5. Promotions and Advertisements (inc. asking for donations)
|
||||
|
||||
You may note place any promotions or advertisements in any meta data including
|
||||
screensthos. This includes asking for donations, promoting online shops,
|
||||
You may not place any promotions or advertisements in any meta data including
|
||||
screenshots. This includes asking for donations, promoting online shops,
|
||||
or linking to personal websites and social media. Please instead use the
|
||||
fields provided on your user profile page to place links to websites and
|
||||
donation pages.
|
||||
|
||||
93
app/flatpages/privacy_policy.md
Normal file
93
app/flatpages/privacy_policy.md
Normal file
@@ -0,0 +1,93 @@
|
||||
title: Privacy Policy
|
||||
|
||||
## What Information is Collected
|
||||
|
||||
**All users:**
|
||||
|
||||
* HTTP requests are logged, with the following information:
|
||||
* Time
|
||||
* IP address
|
||||
* Page URL
|
||||
* Response status code
|
||||
|
||||
**With an account:**
|
||||
|
||||
* Email address
|
||||
* Passwords (hashed and salted using BCrypt)
|
||||
* Profile information, such as website URLs and donation URLs
|
||||
* Comments and threads
|
||||
* Audit log actions (such as edits and logins) and their time stamps
|
||||
|
||||
ContentDB collects usernames of content creators from the forums,
|
||||
as this is required to index forum topics.
|
||||
|
||||
Packages, including releases, screenshots, and any meta information,
|
||||
are not considered personal information.
|
||||
|
||||
Please avoid giving other personal information as we do not want it.
|
||||
|
||||
## How this information is used
|
||||
|
||||
* Logged HTTP requests may be used for debugging ContentDB.
|
||||
* Email addresses are used to:
|
||||
* Provide essential system messages, such as password resets.
|
||||
* Send notifications - the user may configure this to their needs, including opting out.
|
||||
* Passwords are used to authenticate the user.
|
||||
* The audit log is used to record actions that may be harmful
|
||||
* Other information is displayed as part of ContentDB's service.
|
||||
|
||||
## Who has access
|
||||
|
||||
* Only the admin has access to the HTTP requests.
|
||||
The logs may be shared with others to aid in debugging, but care will be taken to remove any personal information.
|
||||
* Encrypted backups may be shared with selected Minetest staff members (moderators + core devs).
|
||||
The keys and the backups themselves are given to different people,
|
||||
requiring at least two staff members to read a backup.
|
||||
* Emails are visible to moderators and the admin.
|
||||
They have access to assist users, and they are not permitted to share email addresses.
|
||||
* Hashing protects passwords from being read whilst stored in the database or in backups.
|
||||
* Profile information is public, including URLs and linked accounts.
|
||||
* The visibility of comments depends on the visibility of threads.
|
||||
They are either public, or visible only to the package author and editors.
|
||||
* The complete audit log is visible to moderators.
|
||||
Users may see their own audit log actions on their account settings page.
|
||||
Owners, maintainers, and editors may be able to see the actions on a package in the future.
|
||||
* We may be required to share information with law enforcement.
|
||||
|
||||
## Location
|
||||
|
||||
The ContentDB production server is currently located in Canada.
|
||||
Backups are stored in the UK.
|
||||
Encrypted backups may be stored in other countries, such as the US or EU.
|
||||
|
||||
By using this service, you give permission for the data to be moved as needed.
|
||||
|
||||
## Period of Retention
|
||||
|
||||
The server uses log rotation, meaning that any logged HTTP requests will be
|
||||
forgotten within a few weeks.
|
||||
|
||||
Usernames may be kept indefinitely, but other user information will be deleted if
|
||||
requested. See below.
|
||||
|
||||
## Removal Requests
|
||||
|
||||
Please [raise a report](https://content.minetest.net/help/reporting/) if you
|
||||
wish to remove your personal information.
|
||||
|
||||
ContentDB keeps a record of each username and forum topic on the forums,
|
||||
for use in indexing mod/game topics. ContentDB also requires the use of a username
|
||||
to uniquely identify a package. Therefore, an author cannot be removed completely
|
||||
from ContentDB if they have any packages or mod/game topics on the forum.
|
||||
|
||||
If we are unable to remove your account for one of the above reasons, your user
|
||||
account will instead be wiped and deactivated, ending up exactly like an author
|
||||
who has not yet joined ContentDB. All personal information will be removed from the profile,
|
||||
and any comments or threads will be deleted.
|
||||
|
||||
## Future Changes to Privacy Policy
|
||||
|
||||
We will alert any future changes to the privacy policy via email and
|
||||
via notices on the ContentDB website.
|
||||
|
||||
By continuing to use this service, you agree to the privacy policy.
|
||||
24
app/logic/LogicError.py
Normal file
24
app/logic/LogicError.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2021 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class LogicError(Exception):
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return repr("LogicError {}: {}".format(self.code, self.message))
|
||||
0
app/logic/__init__.py
Normal file
0
app/logic/__init__.py
Normal file
169
app/logic/packages.py
Normal file
169
app/logic/packages.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2021 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import re
|
||||
import validators
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, License
|
||||
from app.utils import addAuditLog
|
||||
|
||||
|
||||
def check(cond: bool, msg: str):
|
||||
if not cond:
|
||||
raise LogicError(400, msg)
|
||||
|
||||
|
||||
def get_license(name):
|
||||
if type(name) == License:
|
||||
return name
|
||||
|
||||
license = License.query.filter(License.name.ilike(name)).first()
|
||||
if license is None:
|
||||
raise LogicError(400, "Unknown license: " + name)
|
||||
return license
|
||||
|
||||
|
||||
name_re = re.compile("^[a-z0-9_]+$")
|
||||
|
||||
any = "?"
|
||||
ALLOWED_FIELDS = {
|
||||
"type": any,
|
||||
"title": str,
|
||||
"name": str,
|
||||
"short_description": str,
|
||||
"short_desc": str,
|
||||
"tags": list,
|
||||
"content_warnings": list,
|
||||
"license": any,
|
||||
"media_license": any,
|
||||
"long_description": str,
|
||||
"desc": str,
|
||||
"repo": str,
|
||||
"website": str,
|
||||
"issue_tracker": str,
|
||||
"issueTracker": str,
|
||||
"forums": int,
|
||||
}
|
||||
|
||||
ALIASES = {
|
||||
"short_description": "short_desc",
|
||||
"issue_tracker": "issueTracker",
|
||||
"long_description": "desc"
|
||||
}
|
||||
|
||||
|
||||
def is_int(val):
|
||||
try:
|
||||
int(val)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def validate(data: dict):
|
||||
for key, value in data.items():
|
||||
if value is not None:
|
||||
typ = ALLOWED_FIELDS.get(key)
|
||||
check(typ is not None, key + " is not a known field")
|
||||
if typ != any:
|
||||
check(isinstance(value, typ), key + " must be a " + typ.__name__)
|
||||
|
||||
if "name" in data:
|
||||
name = data["name"]
|
||||
check(isinstance(name, str), "Name must be a string")
|
||||
check(bool(name_re.match(name)),
|
||||
"Name can only contain lower case letters (a-z), digits (0-9), and underscores (_)")
|
||||
|
||||
for key in ["repo", "website", "issue_tracker", "issueTracker"]:
|
||||
value = data.get(key)
|
||||
if value is not None:
|
||||
check(value.startswith("http://") or value.startswith("https://"),
|
||||
key + " must start with http:// or https://")
|
||||
|
||||
check(validators.url(value, public=True), key + " must be a valid URL")
|
||||
|
||||
|
||||
def do_edit_package(user: User, package: Package, was_new: bool, data: dict, reason: str = None):
|
||||
if not package.checkPerm(user, Permission.EDIT_PACKAGE):
|
||||
raise LogicError(403, "You do not have permission to edit this package")
|
||||
|
||||
if "name" in data and package.name != data["name"] and \
|
||||
not package.checkPerm(user, Permission.CHANGE_NAME):
|
||||
raise LogicError(403, "You do not have permission to change the package name")
|
||||
|
||||
for alias, to in ALIASES.items():
|
||||
if alias in data:
|
||||
data[to] = data[alias]
|
||||
|
||||
validate(data)
|
||||
|
||||
if "type" in data:
|
||||
data["type"] = PackageType.coerce(data["type"])
|
||||
|
||||
if "license" in data:
|
||||
data["license"] = get_license(data["license"])
|
||||
|
||||
if "media_license" in data:
|
||||
data["media_license"] = get_license(data["media_license"])
|
||||
|
||||
for key in ["name", "title", "short_desc", "desc", "type", "license", "media_license",
|
||||
"repo", "website", "issueTracker", "forums"]:
|
||||
if key in data:
|
||||
setattr(package, key, data[key])
|
||||
|
||||
if package.type == PackageType.TXP:
|
||||
package.license = package.media_license
|
||||
|
||||
if was_new and package.type == PackageType.MOD:
|
||||
m = MetaPackage.GetOrCreate(package.name, {})
|
||||
package.provides.append(m)
|
||||
|
||||
if "tags" in data:
|
||||
package.tags.clear()
|
||||
for tag_id in data["tags"]:
|
||||
if is_int(tag_id):
|
||||
package.tags.append(Tag.query.get(tag_id))
|
||||
else:
|
||||
tag = Tag.query.filter_by(name=tag_id).first()
|
||||
if tag is None:
|
||||
raise LogicError(400, "Unknown tag: " + tag_id)
|
||||
package.tags.append(tag)
|
||||
|
||||
if "content_warnings" in data:
|
||||
package.content_warnings.clear()
|
||||
for warning_id in data["content_warnings"]:
|
||||
if is_int(warning_id):
|
||||
package.content_warnings.append(ContentWarning.query.get(warning_id))
|
||||
else:
|
||||
warning = ContentWarning.query.filter_by(name=warning_id).first()
|
||||
if warning is None:
|
||||
raise LogicError(400, "Unknown warning: " + warning_id)
|
||||
package.content_warnings.append(warning)
|
||||
|
||||
if not was_new:
|
||||
if reason is None:
|
||||
msg = "Edited {}".format(package.title)
|
||||
else:
|
||||
msg = "Edited {} ({})".format(package.title, reason)
|
||||
|
||||
severity = AuditSeverity.NORMAL if user in package.maintainers else AuditSeverity.EDITOR
|
||||
addAuditLog(severity, user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return package
|
||||
90
app/logic/releases.py
Normal file
90
app/logic/releases.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
from celery import uuid
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.logic.uploads import upload_file
|
||||
from app.models import PackageRelease, db, Permission, User, Package, MinetestRelease
|
||||
from app.tasks.importtasks import makeVCSRelease, checkZipRelease
|
||||
from app.utils import AuditSeverity, addAuditLog, nonEmptyOrNone
|
||||
|
||||
|
||||
def check_can_create_release(user: User, package: Package):
|
||||
if not package.checkPerm(user, Permission.MAKE_RELEASE):
|
||||
raise LogicError(403, "You do not have permission to make releases")
|
||||
|
||||
five_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=5)
|
||||
count = package.releases.filter(PackageRelease.releaseDate > five_minutes_ago).count()
|
||||
if count >= 2:
|
||||
raise LogicError(429, "Too many requests, please wait before trying again")
|
||||
|
||||
|
||||
def do_create_vcs_release(user: User, package: Package, title: str, ref: str,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None):
|
||||
check_can_create_release(user, package)
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = title
|
||||
rel.url = ""
|
||||
rel.task_id = uuid()
|
||||
rel.min_rel = min_v
|
||||
rel.max_rel = max_v
|
||||
db.session.add(rel)
|
||||
|
||||
if reason is None:
|
||||
msg = "Created release {}".format(rel.title)
|
||||
else:
|
||||
msg = "Created release {} ({})".format(rel.title, reason)
|
||||
addAuditLog(AuditSeverity.NORMAL, user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
makeVCSRelease.apply_async((rel.id, nonEmptyOrNone(ref)), task_id=rel.task_id)
|
||||
|
||||
return rel
|
||||
|
||||
|
||||
def do_create_zip_release(user: User, package: Package, title: str, file,
|
||||
min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None):
|
||||
check_can_create_release(user, package)
|
||||
|
||||
uploaded_url, uploaded_path = upload_file(file, "zip", "a zip file")
|
||||
|
||||
rel = PackageRelease()
|
||||
rel.package = package
|
||||
rel.title = title
|
||||
rel.url = uploaded_url
|
||||
rel.task_id = uuid()
|
||||
rel.min_rel = min_v
|
||||
rel.max_rel = max_v
|
||||
db.session.add(rel)
|
||||
|
||||
if reason is None:
|
||||
msg = "Created release {}".format(rel.title)
|
||||
else:
|
||||
msg = "Created release {} ({})".format(rel.title, reason)
|
||||
addAuditLog(AuditSeverity.NORMAL, user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
checkZipRelease.apply_async((rel.id, uploaded_path), task_id=rel.task_id)
|
||||
|
||||
return rel
|
||||
58
app/logic/screenshots.py
Normal file
58
app/logic/screenshots.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import datetime
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.logic.uploads import upload_file
|
||||
from app.models import User, Package, PackageScreenshot, Permission, NotificationType, db, AuditSeverity
|
||||
from app.utils import addNotification, addAuditLog
|
||||
|
||||
|
||||
def do_create_screenshot(user: User, package: Package, title: str, file, reason: str = None):
|
||||
thirty_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=30)
|
||||
count = package.screenshots.filter(PackageScreenshot.created_at > thirty_minutes_ago).count()
|
||||
if count >= 20:
|
||||
raise LogicError(429, "Too many requests, please wait before trying again")
|
||||
|
||||
uploaded_url, uploaded_path = upload_file(file, "image", "a PNG or JPG image file")
|
||||
|
||||
counter = 1
|
||||
for screenshot in package.screenshots.all():
|
||||
screenshot.order = counter
|
||||
counter += 1
|
||||
|
||||
ss = PackageScreenshot()
|
||||
ss.package = package
|
||||
ss.title = title or "Untitled"
|
||||
ss.url = uploaded_url
|
||||
ss.approved = package.checkPerm(user, Permission.APPROVE_SCREENSHOT)
|
||||
ss.order = counter
|
||||
db.session.add(ss)
|
||||
|
||||
if reason is None:
|
||||
msg = "Created screenshot {}".format(ss.title)
|
||||
else:
|
||||
msg = "Created screenshot {} ({})".format(ss.title, reason)
|
||||
|
||||
addNotification(package.maintainers, user, NotificationType.PACKAGE_EDIT, msg, package.getDetailsURL(), package)
|
||||
addAuditLog(AuditSeverity.NORMAL, user, msg, package.getDetailsURL(), package)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return ss
|
||||
|
||||
|
||||
def do_order_screenshots(_user: User, package: Package, order: [any]):
|
||||
lookup = {}
|
||||
for screenshot in package.screenshots.all():
|
||||
lookup[screenshot.id] = screenshot
|
||||
|
||||
counter = 1
|
||||
for id in order:
|
||||
try:
|
||||
lookup[int(id)].order = counter
|
||||
counter += 1
|
||||
except KeyError as e:
|
||||
raise LogicError(400, "Unable to find screenshot with id={}".format(id))
|
||||
except ValueError as e:
|
||||
raise LogicError(400, "Invalid number: {}".format(id))
|
||||
|
||||
db.session.commit()
|
||||
61
app/logic/uploads.py
Normal file
61
app/logic/uploads.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import imghdr
|
||||
import os
|
||||
|
||||
from app.logic.LogicError import LogicError
|
||||
from app.models import *
|
||||
from app.utils import randomString
|
||||
|
||||
|
||||
def get_extension(filename):
|
||||
return filename.rsplit(".", 1)[1].lower() if "." in filename else None
|
||||
|
||||
ALLOWED_IMAGES = {"jpeg", "png"}
|
||||
def isAllowedImage(data):
|
||||
return imghdr.what(None, data) in ALLOWED_IMAGES
|
||||
|
||||
def upload_file(file, fileType, fileTypeDesc):
|
||||
if not file or file is None or file.filename == "":
|
||||
raise LogicError(400, "Expected file")
|
||||
|
||||
assert os.path.isdir(app.config["UPLOAD_DIR"]), "UPLOAD_DIR must exist"
|
||||
|
||||
isImage = False
|
||||
if fileType == "image":
|
||||
allowedExtensions = ["jpg", "jpeg", "png"]
|
||||
isImage = True
|
||||
elif fileType == "zip":
|
||||
allowedExtensions = ["zip"]
|
||||
else:
|
||||
raise Exception("Invalid fileType")
|
||||
|
||||
ext = get_extension(file.filename)
|
||||
if ext is None or not ext in allowedExtensions:
|
||||
raise LogicError(400, "Please upload " + fileTypeDesc)
|
||||
|
||||
if isImage and not isAllowedImage(file.stream.read()):
|
||||
raise LogicError(400, "Uploaded image isn't actually an image")
|
||||
|
||||
file.stream.seek(0)
|
||||
|
||||
filename = randomString(10) + "." + ext
|
||||
filepath = os.path.join(app.config["UPLOAD_DIR"], filename)
|
||||
file.save(filepath)
|
||||
|
||||
return "/uploads/" + filename, filepath
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from enum import Enum
|
||||
from app.tasks.emails import sendEmailRaw
|
||||
|
||||
from app.tasks.emails import send_user_email
|
||||
|
||||
|
||||
def _has_newline(line):
|
||||
"""Used by has_bad_header to check for \\r or \\n"""
|
||||
@@ -34,76 +35,57 @@ class FlaskMailSubjectFormatter(logging.Formatter):
|
||||
class FlaskMailTextFormatter(logging.Formatter):
|
||||
pass
|
||||
|
||||
# TODO: hier nog niet tevreden over (vooral logger.error(..., exc_info, stack_info))
|
||||
class FlaskMailHTMLFormatter(logging.Formatter):
|
||||
pre_template = "<h1>%s</h1><pre>%s</pre>"
|
||||
def formatException(self, exc_info):
|
||||
formatted_exception = logging.Handler.formatException(self, exc_info)
|
||||
return FlaskMailHTMLFormatter.pre_template % ("Exception information", formatted_exception)
|
||||
return "<pre>%s</pre>" % formatted_exception
|
||||
def formatStack(self, stack_info):
|
||||
return FlaskMailHTMLFormatter.pre_template % ("<h1>Stack information</h1><pre>%s</pre>", stack_info)
|
||||
return "<pre>%s</pre>" % stack_info
|
||||
|
||||
|
||||
# see: https://github.com/python/cpython/blob/3.6/Lib/logging/__init__.py (class Handler)
|
||||
|
||||
class FlaskMailHandler(logging.Handler):
|
||||
def __init__(self, mailer, subject_template, level=logging.NOTSET):
|
||||
def __init__(self, send_to, subject_template, level=logging.NOTSET):
|
||||
logging.Handler.__init__(self, level)
|
||||
self.mailer = mailer
|
||||
self.send_to = mailer.app.config["MAIL_UTILS_ERROR_SEND_TO"]
|
||||
self.send_to = send_to
|
||||
self.subject_template = subject_template
|
||||
self.html_formatter = None
|
||||
|
||||
def setFormatter(self, text_fmt, html_fmt=None):
|
||||
def setFormatter(self, text_fmt):
|
||||
"""
|
||||
Set the formatters for this handler. Provide at least one formatter.
|
||||
When no text_fmt is provided, no text-part is created for the email body.
|
||||
"""
|
||||
assert (text_fmt, html_fmt) != (None, None), "At least one formatter should be provided"
|
||||
assert text_fmt != None, "At least one formatter should be provided"
|
||||
if type(text_fmt)==str:
|
||||
text_fmt = FlaskMailTextFormatter(text_fmt)
|
||||
self.formatter = text_fmt
|
||||
if type(html_fmt)==str:
|
||||
html_fmt = FlaskMailHTMLFormatter(html_fmt)
|
||||
self.html_formatter = html_fmt
|
||||
|
||||
def getSubject(self, record):
|
||||
fmt = FlaskMailSubjectFormatter(self.subject_template)
|
||||
subject = fmt.format(record)
|
||||
#Since templates can cause header problems, and we rather have a incomplete email then an error, we fix this
|
||||
# Since templates can cause header problems, and we rather have a incomplete email then an error, we fix this
|
||||
if _is_bad_subject(subject):
|
||||
subject="FlaskMailHandler log-entry from %s [original subject is replaced, because it would result in a bad header]" % self.mailer.app.name
|
||||
subject="FlaskMailHandler log-entry from ContentDB [original subject is replaced, because it would result in a bad header]"
|
||||
return subject
|
||||
|
||||
def emit(self, record):
|
||||
text = self.format(record) if self.formatter else None
|
||||
html = self.html_formatter.format(record) if self.html_formatter else None
|
||||
sendEmailRaw.delay(self.send_to, self.getSubject(record), text, html)
|
||||
html = "<pre>{}</pre>".format(text)
|
||||
for email in self.send_to:
|
||||
send_user_email.delay(email, self.getSubject(record), text, html)
|
||||
|
||||
|
||||
def register_mail_error_handler(app, mailer):
|
||||
subject_template = "ContentDB crashed (%(module)s > %(funcName)s)"
|
||||
text_template = """
|
||||
Message type: %(levelname)s
|
||||
Location: %(pathname)s:%(lineno)d
|
||||
Module: %(module)s
|
||||
Function: %(funcName)s
|
||||
Time: %(asctime)s
|
||||
Message:
|
||||
%(message)s"""
|
||||
html_template = """
|
||||
<style>th { text-align: right}</style><table>
|
||||
<tr><th>Message type:</th><td>%(levelname)s</td></tr>
|
||||
<tr> <th>Location:</th><td>%(pathname)s:%(lineno)d</td></tr>
|
||||
<tr> <th>Module:</th><td>%(module)s</td></tr>
|
||||
<tr> <th>Function:</th><td>%(funcName)s</td></tr>
|
||||
<tr> <th>Time:</th><td>%(asctime)s</td></tr>
|
||||
</table>
|
||||
<h2>Message</h2>
|
||||
<pre>%(message)s</pre>"""
|
||||
def build_handler(app):
|
||||
subject_template = "ContentDB %(message)s (%(module)s > %(funcName)s)"
|
||||
text_template = ("Message type: %(levelname)s\n"
|
||||
"Location: %(pathname)s:%(lineno)d\n"
|
||||
"Module: %(module)s\n"
|
||||
"Function: %(funcName)s\n"
|
||||
"Time: %(asctime)s\n"
|
||||
"Message: %(message)s\n\n")
|
||||
|
||||
import logging
|
||||
mail_handler = FlaskMailHandler(mailer, subject_template)
|
||||
mail_handler = FlaskMailHandler(app.config["MAIL_UTILS_ERROR_SEND_TO"], subject_template)
|
||||
mail_handler.setLevel(logging.ERROR)
|
||||
mail_handler.setFormatter(text_template, html_template)
|
||||
app.logger.addHandler(mail_handler)
|
||||
mail_handler.setFormatter(text_template)
|
||||
return mail_handler
|
||||
|
||||
990
app/models.py
990
app/models.py
@@ -1,990 +0,0 @@
|
||||
# Content DB
|
||||
# Copyright (C) 2018 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import enum, datetime
|
||||
|
||||
from app import app, gravatar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask import Flask, url_for
|
||||
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
||||
from flask_migrate import Migrate
|
||||
from flask_user import login_required, UserManager, UserMixin, SQLAlchemyAdapter
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import validates
|
||||
from sqlalchemy_searchable import SearchQueryMixin
|
||||
from sqlalchemy_utils.types import TSVectorType
|
||||
from sqlalchemy_searchable import make_searchable
|
||||
|
||||
|
||||
# Initialise database
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
make_searchable(db.metadata)
|
||||
|
||||
|
||||
class ArticleQuery(BaseQuery, SearchQueryMixin):
|
||||
pass
|
||||
|
||||
|
||||
class UserRank(enum.Enum):
|
||||
BANNED = 0
|
||||
NOT_JOINED = 1
|
||||
NEW_MEMBER = 2
|
||||
MEMBER = 3
|
||||
TRUSTED_MEMBER = 4
|
||||
EDITOR = 5
|
||||
MODERATOR = 6
|
||||
ADMIN = 7
|
||||
|
||||
def atLeast(self, min):
|
||||
return self.value >= min.value
|
||||
|
||||
def getTitle(self):
|
||||
return self.name.replace("_", " ").title()
|
||||
|
||||
def toName(self):
|
||||
return self.name.lower()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return [(choice, choice.getTitle()) for choice in cls]
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, item):
|
||||
return item if type(item) == UserRank else UserRank[item]
|
||||
|
||||
|
||||
class Permission(enum.Enum):
|
||||
EDIT_PACKAGE = "EDIT_PACKAGE"
|
||||
APPROVE_CHANGES = "APPROVE_CHANGES"
|
||||
DELETE_PACKAGE = "DELETE_PACKAGE"
|
||||
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
||||
CHANGE_NAME = "CHANGE_NAME"
|
||||
MAKE_RELEASE = "MAKE_RELEASE"
|
||||
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
||||
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
||||
APPROVE_RELEASE = "APPROVE_RELEASE"
|
||||
APPROVE_NEW = "APPROVE_NEW"
|
||||
CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
|
||||
CHANGE_DNAME = "CHANGE_DNAME"
|
||||
CHANGE_RANK = "CHANGE_RANK"
|
||||
CHANGE_EMAIL = "CHANGE_EMAIL"
|
||||
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
|
||||
SEE_THREAD = "SEE_THREAD"
|
||||
CREATE_THREAD = "CREATE_THREAD"
|
||||
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
|
||||
TOPIC_DISCARD = "TOPIC_DISCARD"
|
||||
|
||||
# Only return true if the permission is valid for *all* contexts
|
||||
# See Package.checkPerm for package-specific contexts
|
||||
def check(self, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if self == Permission.APPROVE_NEW or \
|
||||
self == Permission.APPROVE_CHANGES or \
|
||||
self == Permission.APPROVE_RELEASE or \
|
||||
self == Permission.APPROVE_SCREENSHOT or \
|
||||
self == Permission.SEE_THREAD:
|
||||
return user.rank.atLeast(UserRank.EDITOR)
|
||||
else:
|
||||
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# User authentication information
|
||||
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
|
||||
password = db.Column(db.String(255), nullable=True)
|
||||
reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
|
||||
|
||||
rank = db.Column(db.Enum(UserRank))
|
||||
|
||||
# Account linking
|
||||
github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
||||
forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
||||
|
||||
# User email information
|
||||
email = db.Column(db.String(255), nullable=True, unique=True)
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
|
||||
# User information
|
||||
profile_pic = db.Column(db.String(255), nullable=True, server_default=None)
|
||||
active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
|
||||
display_name = db.Column(db.String(100), nullable=False, server_default="")
|
||||
|
||||
# Links
|
||||
website_url = db.Column(db.String(255), nullable=True, default=None)
|
||||
donate_url = db.Column(db.String(255), nullable=True, default=None)
|
||||
|
||||
# Content
|
||||
notifications = db.relationship("Notification", primaryjoin="User.id==Notification.user_id")
|
||||
|
||||
# causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
|
||||
packages = db.relationship("Package", backref="author", lazy="dynamic")
|
||||
requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
|
||||
threads = db.relationship("Thread", backref="author", lazy="dynamic")
|
||||
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
|
||||
|
||||
def __init__(self, username, active=False, email=None, password=None):
|
||||
self.username = username
|
||||
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
|
||||
self.display_name = username
|
||||
self.active = active
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.rank = UserRank.NOT_JOINED
|
||||
|
||||
def canAccessTodoList(self):
|
||||
return Permission.APPROVE_NEW.check(self) or \
|
||||
Permission.APPROVE_RELEASE.check(self) or \
|
||||
Permission.APPROVE_CHANGES.check(self)
|
||||
|
||||
def isClaimed(self):
|
||||
return self.rank.atLeast(UserRank.NEW_MEMBER)
|
||||
|
||||
def getProfilePicURL(self):
|
||||
if self.profile_pic:
|
||||
return self.profile_pic
|
||||
else:
|
||||
return gravatar(self.email or "")
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to User.checkPerm()")
|
||||
|
||||
# Members can edit their own packages, and editors can edit any packages
|
||||
if perm == Permission.CHANGE_AUTHOR:
|
||||
return user.rank.atLeast(UserRank.EDITOR)
|
||||
elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME:
|
||||
return user.rank.atLeast(UserRank.MODERATOR)
|
||||
elif perm == Permission.CHANGE_EMAIL:
|
||||
return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
|
||||
else:
|
||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
||||
|
||||
def canCommentRL(self):
|
||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||
return ThreadReply.query.filter_by(author=self) \
|
||||
.filter(ThreadReply.created_at > hour_ago).count() < 4
|
||||
|
||||
def canOpenThreadRL(self):
|
||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||
return Thread.query.filter_by(author=self) \
|
||||
.filter(Thread.created_at > hour_ago).count() < 2
|
||||
|
||||
class UserEmailVerification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
email = db.Column(db.String(100))
|
||||
token = db.Column(db.String(32))
|
||||
user = db.relationship("User", foreign_keys=[user_id])
|
||||
|
||||
class Notification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
causer_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
user = db.relationship("User", foreign_keys=[user_id])
|
||||
causer = db.relationship("User", foreign_keys=[causer_id])
|
||||
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
url = db.Column(db.String(200), nullable=True)
|
||||
|
||||
def __init__(self, us, cau, titl, ur):
|
||||
if len(titl) > 100:
|
||||
title = title[:99] + "…"
|
||||
|
||||
self.user = us
|
||||
self.causer = cau
|
||||
self.title = titl
|
||||
self.url = ur
|
||||
|
||||
|
||||
class License(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), nullable=False, unique=True)
|
||||
is_foss = db.Column(db.Boolean, nullable=False, default=True)
|
||||
|
||||
def __init__(self, v, is_foss=True):
|
||||
self.name = v
|
||||
self.is_foss = is_foss
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PackageType(enum.Enum):
|
||||
MOD = "Mod"
|
||||
GAME = "Game"
|
||||
TXP = "Texture Pack"
|
||||
|
||||
def toName(self):
|
||||
return self.name.lower()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
try:
|
||||
return PackageType[name.upper()]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return [(choice, choice.value) for choice in cls]
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, item):
|
||||
return item if type(item) == PackageType else PackageType[item]
|
||||
|
||||
|
||||
class PackagePropertyKey(enum.Enum):
|
||||
name = "Name"
|
||||
title = "Title"
|
||||
short_desc = "Short Description"
|
||||
desc = "Description"
|
||||
type = "Type"
|
||||
license = "License"
|
||||
media_license = "Media License"
|
||||
tags = "Tags"
|
||||
provides = "Provides"
|
||||
repo = "Repository"
|
||||
website = "Website"
|
||||
issueTracker = "Issue Tracker"
|
||||
forums = "Forum Topic ID"
|
||||
|
||||
def convert(self, value):
|
||||
if self == PackagePropertyKey.tags:
|
||||
return ",".join([t.title for t in value])
|
||||
elif self == PackagePropertyKey.provides:
|
||||
return ",".join([t.name for t in value])
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
provides = db.Table("provides",
|
||||
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
|
||||
db.Column("metapackage_id", db.Integer, db.ForeignKey("meta_package.id"), primary_key=True)
|
||||
)
|
||||
|
||||
tags = db.Table("tags",
|
||||
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
|
||||
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
|
||||
)
|
||||
|
||||
class Dependency(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
depender_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id])
|
||||
meta_package_id = db.Column(db.Integer, db.ForeignKey("meta_package.id"), nullable=True)
|
||||
optional = db.Column(db.Boolean, nullable=False, default=False)
|
||||
__table_args__ = (db.UniqueConstraint('depender_id', 'package_id', 'meta_package_id', name='_dependency_uc'), )
|
||||
|
||||
def __init__(self, depender=None, package=None, meta=None):
|
||||
if depender is None:
|
||||
return
|
||||
|
||||
self.depender = depender
|
||||
|
||||
packageProvided = package is not None
|
||||
metaProvided = meta is not None
|
||||
|
||||
if packageProvided and not metaProvided:
|
||||
self.package = package
|
||||
elif metaProvided and not packageProvided:
|
||||
self.meta_package = meta
|
||||
else:
|
||||
raise Exception("Either meta or package must be given, but not both!")
|
||||
|
||||
def __str__(self):
|
||||
if self.package is not None:
|
||||
return self.package.author.username + "/" + self.package.name
|
||||
elif self.meta_package is not None:
|
||||
return self.meta_package.name
|
||||
else:
|
||||
raise Exception("Meta and package are both none!")
|
||||
|
||||
@staticmethod
|
||||
def SpecToList(depender, spec, cache={}):
|
||||
retval = []
|
||||
arr = spec.split(",")
|
||||
|
||||
import re
|
||||
pattern1 = re.compile("^([a-z0-9_]+)$")
|
||||
pattern2 = re.compile("^([A-Za-z0-9_]+)/([a-z0-9_]+)$")
|
||||
|
||||
for x in arr:
|
||||
x = x.strip()
|
||||
if x == "":
|
||||
continue
|
||||
|
||||
if pattern1.match(x):
|
||||
meta = MetaPackage.GetOrCreate(x, cache)
|
||||
retval.append(Dependency(depender, meta=meta))
|
||||
else:
|
||||
m = pattern2.match(x)
|
||||
username = m.group(1)
|
||||
name = m.group(2)
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
raise Exception("Unable to find user " + username)
|
||||
|
||||
package = Package.query.filter_by(author=user, name=name).first()
|
||||
if package is None:
|
||||
raise Exception("Unable to find package " + name + " by " + username)
|
||||
|
||||
retval.append(Dependency(depender, package=package))
|
||||
|
||||
return retval
|
||||
|
||||
|
||||
class Package(db.Model):
|
||||
query_class = ArticleQuery
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Basic details
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
title = db.Column(db.Unicode(100), nullable=False)
|
||||
short_desc = db.Column(db.Unicode(200), nullable=False)
|
||||
desc = db.Column(db.UnicodeText, nullable=True)
|
||||
type = db.Column(db.Enum(PackageType))
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
search_vector = db.Column(TSVectorType("title", "short_desc", "desc"))
|
||||
|
||||
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
||||
license = db.relationship("License", foreign_keys=[license_id])
|
||||
media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
|
||||
media_license = db.relationship("License", foreign_keys=[media_license_id])
|
||||
|
||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
score = db.Column(db.Float, nullable=False, default=0)
|
||||
|
||||
review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
|
||||
review_thread = db.relationship("Thread", foreign_keys=[review_thread_id])
|
||||
|
||||
# Downloads
|
||||
repo = db.Column(db.String(200), nullable=True)
|
||||
website = db.Column(db.String(200), nullable=True)
|
||||
issueTracker = db.Column(db.String(200), nullable=True)
|
||||
forums = db.Column(db.Integer, nullable=True)
|
||||
|
||||
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
|
||||
backref=db.backref("packages", lazy="dynamic", order_by=db.desc("score")))
|
||||
|
||||
dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
|
||||
|
||||
tags = db.relationship("Tag", secondary=tags, lazy="subquery",
|
||||
backref=db.backref("packages", lazy=True))
|
||||
|
||||
releases = db.relationship("PackageRelease", backref="package",
|
||||
lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
|
||||
|
||||
screenshots = db.relationship("PackageScreenshot", backref="package",
|
||||
lazy="dynamic", order_by=db.asc("package_screenshot_id"))
|
||||
|
||||
requests = db.relationship("EditRequest", backref="package",
|
||||
lazy="dynamic")
|
||||
|
||||
def __init__(self, package=None):
|
||||
if package is None:
|
||||
return
|
||||
|
||||
self.author_id = package.author_id
|
||||
self.created_at = package.created_at
|
||||
self.approved = package.approved
|
||||
|
||||
for e in PackagePropertyKey:
|
||||
setattr(self, e.name, getattr(package, e.name))
|
||||
|
||||
def getIsFOSS(self):
|
||||
return self.license.is_foss and self.media_license.is_foss
|
||||
|
||||
def getState(self):
|
||||
if self.approved:
|
||||
return "approved"
|
||||
elif self.review_thread_id:
|
||||
return "thread"
|
||||
elif (self.type == PackageType.GAME or \
|
||||
self.type == PackageType.TXP) and \
|
||||
self.screenshots.count() == 0:
|
||||
return "wip"
|
||||
elif not self.getDownloadRelease():
|
||||
return "wip"
|
||||
elif "Other" in self.license.name or "Other" in self.media_license.name:
|
||||
return "license"
|
||||
else:
|
||||
return "ready"
|
||||
|
||||
def getAsDictionaryKey(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"author": self.author.display_name,
|
||||
"type": self.type.toName(),
|
||||
}
|
||||
|
||||
def getAsDictionaryShort(self, base_url, version=None, protonum=None):
|
||||
tnurl = self.getThumbnailURL(1)
|
||||
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||
return {
|
||||
"name": self.name,
|
||||
"title": self.title,
|
||||
"author": self.author.display_name,
|
||||
"short_description": self.short_desc,
|
||||
"type": self.type.toName(),
|
||||
"release": release and release.id,
|
||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||
"score": round(self.score * 10) / 10
|
||||
}
|
||||
|
||||
def getAsDictionary(self, base_url, version=None, protonum=None):
|
||||
tnurl = self.getThumbnailURL(1)
|
||||
release = self.getDownloadRelease(version=version, protonum=protonum)
|
||||
return {
|
||||
"author": self.author.display_name,
|
||||
"name": self.name,
|
||||
"title": self.title,
|
||||
"short_description": self.short_desc,
|
||||
"desc": self.desc,
|
||||
"type": self.type.toName(),
|
||||
"created_at": self.created_at,
|
||||
|
||||
"license": self.license.name,
|
||||
"media_license": self.media_license.name,
|
||||
|
||||
"repo": self.repo,
|
||||
"website": self.website,
|
||||
"issue_tracker": self.issueTracker,
|
||||
"forums": self.forums,
|
||||
|
||||
"provides": [x.name for x in self.provides],
|
||||
"thumbnail": (base_url + tnurl) if tnurl is not None else None,
|
||||
"screenshots": [base_url + ss.url for ss in self.screenshots],
|
||||
|
||||
"url": base_url + self.getDownloadURL(),
|
||||
"release": release and release.id,
|
||||
|
||||
"score": round(self.score * 10) / 10
|
||||
}
|
||||
|
||||
def getThumbnailURL(self, level=2):
|
||||
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
|
||||
return screenshot.getThumbnailURL(level) if screenshot is not None else None
|
||||
|
||||
def getMainScreenshotURL(self):
|
||||
screenshot = self.screenshots.filter_by(approved=True).order_by(db.asc(PackageScreenshot.id)).first()
|
||||
return screenshot.url if screenshot is not None else None
|
||||
|
||||
def getDetailsURL(self):
|
||||
return url_for("packages.view",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("packages.create_edit",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getApproveURL(self):
|
||||
return url_for("packages.approve",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getRemoveURL(self):
|
||||
return url_for("packages.remove",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getNewScreenshotURL(self):
|
||||
return url_for("packages.create_screenshot",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getCreateReleaseURL(self):
|
||||
return url_for("packages.create_release",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getCreateEditRequestURL(self):
|
||||
return url_for("create_edit_editrequest_page",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getBulkReleaseURL(self):
|
||||
return url_for("packages.bulk_change_release",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getDownloadURL(self):
|
||||
return url_for("packages.download",
|
||||
author=self.author.username, name=self.name)
|
||||
|
||||
def getDownloadRelease(self, version=None, protonum=None):
|
||||
if version is None and protonum is not None:
|
||||
version = MinetestRelease.query.filter(MinetestRelease.protocol >= int(protonum)).first()
|
||||
if version is not None:
|
||||
version = version.id
|
||||
else:
|
||||
version = 10000000
|
||||
|
||||
|
||||
for rel in self.releases:
|
||||
if rel.approved and (version is None or
|
||||
((rel.min_rel is None or rel.min_rel_id <= version) and \
|
||||
(rel.max_rel is None or rel.max_rel_id >= version))):
|
||||
return rel
|
||||
|
||||
return None
|
||||
|
||||
def getDownloadCount(self):
|
||||
counter = 0
|
||||
for release in self.releases:
|
||||
counter += release.downloads
|
||||
return counter
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to Package.checkPerm()")
|
||||
|
||||
isOwner = user == self.author
|
||||
|
||||
if perm == Permission.CREATE_THREAD:
|
||||
return user.rank.atLeast(UserRank.MEMBER)
|
||||
|
||||
# Members can edit their own packages, and editors can edit any packages
|
||||
if perm == Permission.MAKE_RELEASE or perm == Permission.ADD_SCREENSHOTS:
|
||||
return isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
|
||||
if isOwner:
|
||||
return user.rank.atLeast(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
|
||||
else:
|
||||
return user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
# Anyone can change the package name when not approved, but only editors when approved
|
||||
elif perm == Permission.CHANGE_NAME:
|
||||
return not self.approved or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
# Editors can change authors and approve new packages
|
||||
elif perm == Permission.APPROVE_NEW or perm == Permission.CHANGE_AUTHOR:
|
||||
return user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
elif perm == Permission.APPROVE_RELEASE or perm == Permission.APPROVE_SCREENSHOT:
|
||||
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
|
||||
|
||||
# Moderators can delete packages
|
||||
elif perm == Permission.DELETE_PACKAGE or perm == Permission.UNAPPROVE_PACKAGE \
|
||||
or perm == Permission.CHANGE_RELEASE_URL:
|
||||
return user.rank.atLeast(UserRank.MODERATOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to packages".format(perm.name))
|
||||
|
||||
def setStartScore(self):
|
||||
downloads = db.session.query(func.sum(PackageRelease.downloads)). \
|
||||
filter(PackageRelease.package_id == self.id).scalar() or 0
|
||||
|
||||
forum_score = 0
|
||||
forum_bonus = 0
|
||||
topic = self.forums and ForumTopic.query.get(self.forums)
|
||||
if topic:
|
||||
months = (datetime.datetime.now() - topic.created_at).days / 30
|
||||
years = months / 12
|
||||
forum_score = topic.views / max(years, 0.0416) + 80*min(max(months, 0.5), 6)
|
||||
forum_bonus = topic.views + topic.posts
|
||||
|
||||
self.score = max(downloads, forum_score * 0.6) + forum_bonus
|
||||
|
||||
if self.getMainScreenshotURL() is None:
|
||||
self.score *= 0.8
|
||||
|
||||
if not self.license.is_foss or not self.media_license.is_foss:
|
||||
self.score *= 0.1
|
||||
|
||||
|
||||
class MetaPackage(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
dependencies = db.relationship("Dependency", backref="meta_package", lazy="dynamic")
|
||||
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@staticmethod
|
||||
def ListToSpec(list):
|
||||
return ",".join([str(x) for x in list])
|
||||
|
||||
@staticmethod
|
||||
def GetOrCreate(name, cache={}):
|
||||
mp = cache.get(name)
|
||||
if mp is None:
|
||||
mp = MetaPackage.query.filter_by(name=name).first()
|
||||
|
||||
if mp is None:
|
||||
mp = MetaPackage(name)
|
||||
db.session.add(mp)
|
||||
|
||||
cache[name] = mp
|
||||
return mp
|
||||
|
||||
@staticmethod
|
||||
def SpecToList(spec, cache={}):
|
||||
retval = []
|
||||
arr = spec.split(",")
|
||||
|
||||
import re
|
||||
pattern = re.compile("^([a-z0-9_]+)$")
|
||||
|
||||
for x in arr:
|
||||
x = x.strip()
|
||||
if x == "":
|
||||
continue
|
||||
|
||||
if not pattern.match(x):
|
||||
continue
|
||||
|
||||
retval.append(MetaPackage.GetOrCreate(x, cache))
|
||||
|
||||
return retval
|
||||
|
||||
class Tag(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
backgroundColor = db.Column(db.String(6), nullable=False)
|
||||
textColor = db.Column(db.String(6), nullable=False)
|
||||
|
||||
def __init__(self, title, backgroundColor="000000", textColor="ffffff"):
|
||||
self.title = title
|
||||
self.backgroundColor = backgroundColor
|
||||
self.textColor = textColor
|
||||
|
||||
import re
|
||||
regex = re.compile("[^a-z_]")
|
||||
self.name = regex.sub("", self.title.lower().replace(" ", "_"))
|
||||
|
||||
|
||||
class MinetestRelease(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
protocol = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
|
||||
def getActual(self):
|
||||
return None if self.name == "None" else self
|
||||
|
||||
|
||||
class PackageRelease(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
releaseDate = db.Column(db.DateTime, nullable=False)
|
||||
url = db.Column(db.String(200), nullable=False)
|
||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||
task_id = db.Column(db.String(37), nullable=True)
|
||||
commit_hash = db.Column(db.String(41), nullable=True, default=None)
|
||||
downloads = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])
|
||||
|
||||
max_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
|
||||
max_rel = db.relationship("MinetestRelease", foreign_keys=[max_rel_id])
|
||||
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("packages.edit_release",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getDownloadURL(self):
|
||||
return url_for("packages.download_release",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.releaseDate = datetime.datetime.now()
|
||||
|
||||
def approve(self, user):
|
||||
if self.package.approved and \
|
||||
not self.package.checkPerm(user, Permission.APPROVE_RELEASE):
|
||||
return False
|
||||
|
||||
assert(self.task_id is None and self.url is not None and self.url != "")
|
||||
|
||||
self.approved = True
|
||||
return True
|
||||
|
||||
|
||||
class PackageReview(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||
recommend = db.Column(db.Boolean, nullable=False, default=True)
|
||||
|
||||
|
||||
class PackageScreenshot(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
url = db.Column(db.String(100), nullable=False)
|
||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("packages.edit_screenshot",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getThumbnailURL(self, level=2):
|
||||
return self.url.replace("/uploads/", ("/thumbnails/{:d}/").format(level))
|
||||
|
||||
|
||||
|
||||
class EditRequest(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"))
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
desc = db.Column(db.String(1000), nullable=True)
|
||||
|
||||
# 0 - open
|
||||
# 1 - merged
|
||||
# 2 - rejected
|
||||
status = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
changes = db.relationship("EditRequestChange", backref="request",
|
||||
lazy="dynamic")
|
||||
|
||||
def getURL(self):
|
||||
return url_for("view_editrequest_page",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getApproveURL(self):
|
||||
return url_for("approve_editrequest_page",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getRejectURL(self):
|
||||
return url_for("reject_editrequest_page",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def getEditURL(self):
|
||||
return url_for("create_edit_editrequest_page",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
id=self.id)
|
||||
|
||||
def applyAll(self, package):
|
||||
for change in self.changes:
|
||||
change.apply(package)
|
||||
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to EditRequest.checkPerm()")
|
||||
|
||||
isOwner = user == self.author
|
||||
|
||||
# Members can edit their own packages, and editors can edit any packages
|
||||
if perm == Permission.EDIT_EDITREQUEST:
|
||||
return isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to packages".format(perm.name))
|
||||
|
||||
|
||||
|
||||
|
||||
class EditRequestChange(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
request_id = db.Column(db.Integer, db.ForeignKey("edit_request.id"))
|
||||
key = db.Column(db.Enum(PackagePropertyKey), nullable=False)
|
||||
|
||||
# TODO: make diff instead
|
||||
oldValue = db.Column(db.Text, nullable=True)
|
||||
newValue = db.Column(db.Text, nullable=True)
|
||||
|
||||
def apply(self, package):
|
||||
if self.key == PackagePropertyKey.tags:
|
||||
package.tags.clear()
|
||||
for tagTitle in self.newValue.split(","):
|
||||
tag = Tag.query.filter_by(title=tagTitle.strip()).first()
|
||||
package.tags.append(tag)
|
||||
|
||||
else:
|
||||
setattr(package, self.key.name, self.newValue)
|
||||
|
||||
|
||||
watchers = db.Table("watchers",
|
||||
db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
|
||||
db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
|
||||
)
|
||||
|
||||
class Thread(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id])
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
private = db.Column(db.Boolean, server_default="0")
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
|
||||
|
||||
watchers = db.relationship("User", secondary=watchers, lazy="subquery", \
|
||||
backref=db.backref("watching", lazy=True))
|
||||
|
||||
|
||||
def getSubscribeURL(self):
|
||||
return url_for("threads.subscribe",
|
||||
id=self.id)
|
||||
|
||||
def getUnsubscribeURL(self):
|
||||
return url_for("threads.unsubscribe",
|
||||
id=self.id)
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return not self.private
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to Thread.checkPerm()")
|
||||
|
||||
isOwner = user == self.author or (self.package is not None and self.package.author == user)
|
||||
|
||||
if perm == Permission.SEE_THREAD:
|
||||
return not self.private or isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to threads".format(perm.name))
|
||||
|
||||
class ThreadReply(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||
comment = db.Column(db.String(500), nullable=False)
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
|
||||
"minetest.net", "dropboxusercontent.com", "4shared.com", \
|
||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
|
||||
"imageshack.com", "imgur.com"]
|
||||
|
||||
class ForumTopic(db.Model):
|
||||
topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
author = db.relationship("User")
|
||||
|
||||
wip = db.Column(db.Boolean, server_default="0")
|
||||
discarded = db.Column(db.Boolean, server_default="0")
|
||||
|
||||
type = db.Column(db.Enum(PackageType), nullable=False)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
name = db.Column(db.String(30), nullable=True)
|
||||
link = db.Column(db.String(200), nullable=True)
|
||||
|
||||
posts = db.Column(db.Integer, nullable=False)
|
||||
views = db.Column(db.Integer, nullable=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
def getRepoURL(self):
|
||||
if self.link is None:
|
||||
return None
|
||||
|
||||
for item in REPO_BLACKLIST:
|
||||
if item in self.link:
|
||||
return None
|
||||
|
||||
return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
|
||||
|
||||
def getAsDictionary(self):
|
||||
return {
|
||||
"author": self.author.username,
|
||||
"name": self.name,
|
||||
"type": self.type.toName(),
|
||||
"title": self.title,
|
||||
"id": self.topic_id,
|
||||
"link": self.link,
|
||||
"posts": self.posts,
|
||||
"views": self.views,
|
||||
"is_wip": self.wip,
|
||||
"discarded": self.discarded,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
}
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to ForumTopic.checkPerm()")
|
||||
|
||||
if perm == Permission.TOPIC_DISCARD:
|
||||
return self.author == user or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to topics".format(perm.name))
|
||||
|
||||
|
||||
# Setup Flask-User
|
||||
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
|
||||
user_manager = UserManager(db_adapter, app) # Initialize Flask-User
|
||||
177
app/models/__init__.py
Normal file
177
app/models/__init__.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy_searchable import make_searchable
|
||||
|
||||
from app import app
|
||||
|
||||
# Initialise database
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
make_searchable(db.metadata)
|
||||
|
||||
|
||||
from .packages import *
|
||||
from .users import *
|
||||
from .threads import *
|
||||
|
||||
|
||||
class APIToken(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
access_token = db.Column(db.String(34), unique=True, nullable=False)
|
||||
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
|
||||
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
owner = db.relationship("User", foreign_keys=[owner_id], back_populates="tokens")
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id], back_populates="tokens")
|
||||
|
||||
def canOperateOnPackage(self, package):
|
||||
if self.package and self.package != package:
|
||||
return False
|
||||
|
||||
return package.author == self.owner
|
||||
|
||||
|
||||
class AuditSeverity(enum.Enum):
|
||||
NORMAL = 0 # Normal user changes
|
||||
USER = 1 # Security user changes
|
||||
EDITOR = 2 # Editor changes
|
||||
MODERATION = 3 # Destructive / moderator changes
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def getTitle(self):
|
||||
return self.name.replace("_", " ").title()
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return [(choice, choice.getTitle()) for choice in cls]
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, item):
|
||||
return item if type(item) == AuditSeverity else AuditSeverity[item]
|
||||
|
||||
|
||||
class AuditLogEntry(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
causer_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
|
||||
causer = db.relationship("User", foreign_keys=[causer_id], back_populates="audit_log_entries")
|
||||
|
||||
severity = db.Column(db.Enum(AuditSeverity), nullable=False)
|
||||
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
url = db.Column(db.String(200), nullable=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id], back_populates="audit_log_entries")
|
||||
|
||||
description = db.Column(db.Text, nullable=True, default=None)
|
||||
|
||||
def __init__(self, causer, severity, title, url, package=None, description=None):
|
||||
if len(title) > 100:
|
||||
title = title[:99] + "…"
|
||||
|
||||
self.causer = causer
|
||||
self.severity = severity
|
||||
self.title = title
|
||||
self.url = url
|
||||
self.package = package
|
||||
self.description = description
|
||||
|
||||
|
||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com",
|
||||
"minetest.net", "dropboxusercontent.com", "4shared.com",
|
||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net",
|
||||
"imageshack.com", "imgur.com"]
|
||||
|
||||
|
||||
class ForumTopic(db.Model):
|
||||
topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
author = db.relationship("User")
|
||||
|
||||
wip = db.Column(db.Boolean, server_default="0")
|
||||
discarded = db.Column(db.Boolean, server_default="0")
|
||||
|
||||
type = db.Column(db.Enum(PackageType), nullable=False)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
name = db.Column(db.String(30), nullable=True)
|
||||
link = db.Column(db.String(200), nullable=True)
|
||||
|
||||
posts = db.Column(db.Integer, nullable=False)
|
||||
views = db.Column(db.Integer, nullable=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
def getRepoURL(self):
|
||||
if self.link is None:
|
||||
return None
|
||||
|
||||
for item in REPO_BLACKLIST:
|
||||
if item in self.link:
|
||||
return None
|
||||
|
||||
return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
|
||||
|
||||
def getAsDictionary(self):
|
||||
return {
|
||||
"author": self.author.username,
|
||||
"name": self.name,
|
||||
"type": self.type.toName(),
|
||||
"title": self.title,
|
||||
"id": self.topic_id,
|
||||
"link": self.link,
|
||||
"posts": self.posts,
|
||||
"views": self.views,
|
||||
"is_wip": self.wip,
|
||||
"discarded": self.discarded,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
}
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to ForumTopic.checkPerm()")
|
||||
|
||||
if perm == Permission.TOPIC_DISCARD:
|
||||
return self.author == user or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to topics".format(perm.name))
|
||||
|
||||
|
||||
if app.config.get("LOG_SQL"):
|
||||
import logging
|
||||
logging.basicConfig()
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
||||
1008
app/models/packages.py
Normal file
1008
app/models/packages.py
Normal file
File diff suppressed because it is too large
Load Diff
154
app/models/threads.py
Normal file
154
app/models/threads.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from . import db
|
||||
from .users import Permission, UserRank
|
||||
from .packages import Package
|
||||
|
||||
|
||||
watchers = db.Table("watchers",
|
||||
db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
|
||||
db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
|
||||
)
|
||||
|
||||
|
||||
class Thread(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id], back_populates="threads")
|
||||
|
||||
is_review_thread = db.relationship("Package", foreign_keys=[Package.review_thread_id], back_populates="review_thread")
|
||||
|
||||
review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), nullable=True)
|
||||
review = db.relationship("PackageReview", foreign_keys=[review_id], cascade="all, delete")
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
author = db.relationship("User", back_populates="threads", foreign_keys=[author_id])
|
||||
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
private = db.Column(db.Boolean, server_default="0", nullable=False)
|
||||
|
||||
locked = db.Column(db.Boolean, server_default="0", nullable=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
replies = db.relationship("ThreadReply", back_populates="thread", lazy="dynamic",
|
||||
order_by=db.asc("thread_reply_id"), cascade="all, delete, delete-orphan")
|
||||
|
||||
watchers = db.relationship("User", secondary=watchers, backref="watching")
|
||||
|
||||
def getViewURL(self):
|
||||
return url_for("threads.view", id=self.id, _external=False)
|
||||
|
||||
def getSubscribeURL(self):
|
||||
return url_for("threads.subscribe", id=self.id)
|
||||
|
||||
def getUnsubscribeURL(self):
|
||||
return url_for("threads.unsubscribe", id=self.id)
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return perm == Permission.SEE_THREAD and not self.private
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to Thread.checkPerm()")
|
||||
|
||||
isMaintainer = user == self.author or (self.package is not None and self.package.author == user)
|
||||
if self.package:
|
||||
isMaintainer = isMaintainer or user in self.package.maintainers
|
||||
|
||||
canSee = not self.private or isMaintainer or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
if perm == Permission.SEE_THREAD:
|
||||
return canSee
|
||||
|
||||
elif perm == Permission.COMMENT_THREAD:
|
||||
return canSee and (not self.locked or user.rank.atLeast(UserRank.MODERATOR))
|
||||
|
||||
elif perm == Permission.LOCK_THREAD or perm == Permission.DELETE_THREAD:
|
||||
return user.rank.atLeast(UserRank.MODERATOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to threads".format(perm.name))
|
||||
|
||||
def get_latest_reply(self):
|
||||
return ThreadReply.query.filter_by(thread_id=self.id).order_by(db.desc(ThreadReply.id)).first()
|
||||
|
||||
|
||||
class ThreadReply(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||
thread = db.relationship("Thread", back_populates="replies", foreign_keys=[thread_id])
|
||||
|
||||
comment = db.Column(db.String(2000), nullable=False)
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
author = db.relationship("User", back_populates="replies", foreign_keys=[author_id])
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to ThreadReply.checkPerm()")
|
||||
|
||||
if perm == Permission.EDIT_REPLY:
|
||||
return user == self.author and user.rank.atLeast(UserRank.MEMBER) and not self.thread.locked
|
||||
|
||||
elif perm == Permission.DELETE_REPLY:
|
||||
return user.rank.atLeast(UserRank.MODERATOR) and self.thread.replies[0] != self
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to threads".format(perm.name))
|
||||
|
||||
|
||||
class PackageReview(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id], back_populates="reviews")
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
author = db.relationship("User", foreign_keys=[author_id], back_populates="reviews")
|
||||
|
||||
recommends = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
thread = db.relationship("Thread", uselist=False, back_populates="review")
|
||||
|
||||
def asSign(self):
|
||||
return 1 if self.recommends else -1
|
||||
|
||||
def getEditURL(self):
|
||||
return self.package.getReviewURL()
|
||||
|
||||
def getDeleteURL(self):
|
||||
return url_for("packages.delete_review",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name)
|
||||
468
app/models/users.py
Normal file
468
app/models/users.py
Normal file
@@ -0,0 +1,468 @@
|
||||
# ContentDB
|
||||
# Copyright (C) 2018-21 rubenwardy
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import datetime
|
||||
import enum
|
||||
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import desc, text
|
||||
|
||||
from app import gravatar
|
||||
from . import db
|
||||
|
||||
|
||||
class UserRank(enum.Enum):
|
||||
BANNED = 0
|
||||
NOT_JOINED = 1
|
||||
NEW_MEMBER = 2
|
||||
MEMBER = 3
|
||||
TRUSTED_MEMBER = 4
|
||||
EDITOR = 5
|
||||
BOT = 6
|
||||
MODERATOR = 7
|
||||
ADMIN = 8
|
||||
|
||||
def atLeast(self, min):
|
||||
return self.value >= min.value
|
||||
|
||||
def getTitle(self):
|
||||
return self.name.replace("_", " ").title()
|
||||
|
||||
def toName(self):
|
||||
return self.name.lower()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return [(choice, choice.getTitle()) for choice in cls]
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, item):
|
||||
return item if type(item) == UserRank else UserRank[item]
|
||||
|
||||
|
||||
class Permission(enum.Enum):
|
||||
EDIT_PACKAGE = "EDIT_PACKAGE"
|
||||
APPROVE_CHANGES = "APPROVE_CHANGES"
|
||||
DELETE_PACKAGE = "DELETE_PACKAGE"
|
||||
CHANGE_AUTHOR = "CHANGE_AUTHOR"
|
||||
CHANGE_NAME = "CHANGE_NAME"
|
||||
MAKE_RELEASE = "MAKE_RELEASE"
|
||||
DELETE_RELEASE = "DELETE_RELEASE"
|
||||
ADD_SCREENSHOTS = "ADD_SCREENSHOTS"
|
||||
REIMPORT_META = "REIMPORT_META"
|
||||
APPROVE_SCREENSHOT = "APPROVE_SCREENSHOT"
|
||||
APPROVE_RELEASE = "APPROVE_RELEASE"
|
||||
APPROVE_NEW = "APPROVE_NEW"
|
||||
EDIT_TAGS = "EDIT_TAGS"
|
||||
CREATE_TAG = "CREATE_TAG"
|
||||
CHANGE_RELEASE_URL = "CHANGE_RELEASE_URL"
|
||||
CHANGE_USERNAMES = "CHANGE_USERNAMES"
|
||||
CHANGE_RANK = "CHANGE_RANK"
|
||||
CHANGE_EMAIL = "CHANGE_EMAIL"
|
||||
SEE_THREAD = "SEE_THREAD"
|
||||
CREATE_THREAD = "CREATE_THREAD"
|
||||
COMMENT_THREAD = "COMMENT_THREAD"
|
||||
LOCK_THREAD = "LOCK_THREAD"
|
||||
DELETE_THREAD = "DELETE_THREAD"
|
||||
DELETE_REPLY = "DELETE_REPLY"
|
||||
EDIT_REPLY = "EDIT_REPLY"
|
||||
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
|
||||
TOPIC_DISCARD = "TOPIC_DISCARD"
|
||||
CREATE_TOKEN = "CREATE_TOKEN"
|
||||
EDIT_MAINTAINERS = "EDIT_MAINTAINERS"
|
||||
CHANGE_PROFILE_URLS = "CHANGE_PROFILE_URLS"
|
||||
|
||||
# Only return true if the permission is valid for *all* contexts
|
||||
# See Package.checkPerm for package-specific contexts
|
||||
def check(self, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if self == Permission.APPROVE_NEW or \
|
||||
self == Permission.APPROVE_CHANGES or \
|
||||
self == Permission.APPROVE_RELEASE or \
|
||||
self == Permission.APPROVE_SCREENSHOT or \
|
||||
self == Permission.EDIT_TAGS or \
|
||||
self == Permission.CREATE_TAG or \
|
||||
self == Permission.SEE_THREAD:
|
||||
return user.rank.atLeast(UserRank.EDITOR)
|
||||
else:
|
||||
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
||||
|
||||
@staticmethod
|
||||
def checkPerm(user, perm):
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to Permission.check")
|
||||
|
||||
return perm.check(user)
|
||||
|
||||
|
||||
def display_name_default(context):
|
||||
return context.get_current_parameters()["username"]
|
||||
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# User authentication information
|
||||
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
|
||||
password = db.Column(db.String(255), nullable=True, server_default=None)
|
||||
reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
|
||||
|
||||
def get_id(self):
|
||||
return self.username
|
||||
|
||||
rank = db.Column(db.Enum(UserRank), nullable=False)
|
||||
|
||||
# Account linking
|
||||
github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
||||
forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
|
||||
|
||||
# Access token for webhook setup
|
||||
github_access_token = db.Column(db.String(50), nullable=True, server_default=None)
|
||||
|
||||
# User email information
|
||||
email = db.Column(db.String(255), nullable=True, unique=True)
|
||||
email_confirmed_at = db.Column(db.DateTime(), nullable=True)
|
||||
|
||||
# User information
|
||||
profile_pic = db.Column(db.String(255), nullable=True, server_default=None)
|
||||
is_active = db.Column("is_active", db.Boolean, nullable=False, server_default="0")
|
||||
display_name = db.Column(db.String(100), nullable=False, default=display_name_default)
|
||||
|
||||
# Links
|
||||
website_url = db.Column(db.String(255), nullable=True, default=None)
|
||||
donate_url = db.Column(db.String(255), nullable=True, default=None)
|
||||
|
||||
# Content
|
||||
notifications = db.relationship("Notification", foreign_keys="Notification.user_id",
|
||||
order_by=desc(text("Notification.created_at")), back_populates="user", cascade="all, delete, delete-orphan")
|
||||
caused_notifications = db.relationship("Notification", foreign_keys="Notification.causer_id",
|
||||
back_populates="causer", cascade="all, delete, delete-orphan", lazy="dynamic")
|
||||
notification_preferences = db.relationship("UserNotificationPreferences", uselist=False, back_populates="user",
|
||||
cascade="all, delete, delete-orphan")
|
||||
|
||||
email_verifications = db.relationship("UserEmailVerification", foreign_keys="UserEmailVerification.user_id",
|
||||
back_populates="user", cascade="all, delete, delete-orphan", lazy="dynamic")
|
||||
|
||||
audit_log_entries = db.relationship("AuditLogEntry", foreign_keys="AuditLogEntry.causer_id", back_populates="causer",
|
||||
order_by=desc("audit_log_entry_created_at"), lazy="dynamic")
|
||||
|
||||
maintained_packages = db.relationship("Package", lazy="dynamic", secondary="maintainers", order_by=db.asc("package_title"))
|
||||
|
||||
packages = db.relationship("Package", back_populates="author", lazy="dynamic", order_by=db.asc("package_title"))
|
||||
reviews = db.relationship("PackageReview", back_populates="author", order_by=db.desc("package_review_created_at"), cascade="all, delete, delete-orphan")
|
||||
tokens = db.relationship("APIToken", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
threads = db.relationship("Thread", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
|
||||
def __init__(self, username=None, active=False, email=None, password=None):
|
||||
self.username = username
|
||||
self.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
|
||||
self.display_name = username
|
||||
self.is_active = active
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.rank = UserRank.NOT_JOINED
|
||||
|
||||
def canAccessTodoList(self):
|
||||
return Permission.APPROVE_NEW.check(self) or \
|
||||
Permission.APPROVE_RELEASE.check(self) or \
|
||||
Permission.APPROVE_CHANGES.check(self)
|
||||
|
||||
def isClaimed(self):
|
||||
return self.rank.atLeast(UserRank.NEW_MEMBER)
|
||||
|
||||
def getProfilePicURL(self):
|
||||
if self.profile_pic:
|
||||
return self.profile_pic
|
||||
elif self.rank == UserRank.BOT:
|
||||
return "/static/bot_avatar.png"
|
||||
else:
|
||||
return gravatar(self.email or "")
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to User.checkPerm()")
|
||||
|
||||
# Members can edit their own packages, and editors can edit any packages
|
||||
if perm == Permission.CHANGE_AUTHOR:
|
||||
return user.rank.atLeast(UserRank.EDITOR)
|
||||
elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_USERNAMES:
|
||||
return user.rank.atLeast(UserRank.MODERATOR)
|
||||
elif perm == Permission.CHANGE_EMAIL or perm == Permission.CHANGE_PROFILE_URLS:
|
||||
return user == self or user.rank.atLeast(UserRank.ADMIN)
|
||||
elif perm == Permission.CREATE_TOKEN:
|
||||
if user == self:
|
||||
return user.rank.atLeast(UserRank.MEMBER)
|
||||
else:
|
||||
return user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank)
|
||||
else:
|
||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
||||
|
||||
def canCommentRL(self):
|
||||
from app.models import ThreadReply
|
||||
|
||||
factor = 1
|
||||
if self.rank.atLeast(UserRank.ADMIN):
|
||||
return True
|
||||
elif self.rank.atLeast(UserRank.TRUSTED_MEMBER):
|
||||
factor *= 2
|
||||
|
||||
one_min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=1)
|
||||
if ThreadReply.query.filter_by(author=self) \
|
||||
.filter(ThreadReply.created_at > one_min_ago).count() >= 3 * factor:
|
||||
return False
|
||||
|
||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||
if ThreadReply.query.filter_by(author=self) \
|
||||
.filter(ThreadReply.created_at > hour_ago).count() >= 20 * factor:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def canOpenThreadRL(self):
|
||||
from app.models import Thread
|
||||
|
||||
factor = 1
|
||||
if self.rank.atLeast(UserRank.ADMIN):
|
||||
return True
|
||||
elif self.rank.atLeast(UserRank.TRUSTED_MEMBER):
|
||||
factor *= 5
|
||||
|
||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||
return Thread.query.filter_by(author=self) \
|
||||
.filter(Thread.created_at > hour_ago).count() < 2 * factor
|
||||
|
||||
def __eq__(self, other):
|
||||
if other is None:
|
||||
return False
|
||||
|
||||
if not self.is_authenticated or not other.is_authenticated:
|
||||
return False
|
||||
|
||||
assert self.id > 0
|
||||
return self.id == other.id
|
||||
|
||||
def can_see_edit_profile(self, current_user):
|
||||
return self.checkPerm(current_user, Permission.CHANGE_USERNAMES) or \
|
||||
self.checkPerm(current_user, Permission.CHANGE_EMAIL) or \
|
||||
self.checkPerm(current_user, Permission.CHANGE_RANK)
|
||||
|
||||
def can_delete(self):
|
||||
from app.models import ForumTopic
|
||||
return self.packages.count() == 0 and ForumTopic.query.filter_by(author=self).count() == 0
|
||||
|
||||
|
||||
class UserEmailVerification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
email = db.Column(db.String(100), nullable=False)
|
||||
token = db.Column(db.String(32), nullable=True)
|
||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="email_verifications")
|
||||
is_password_reset = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
|
||||
class EmailSubscription(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(100), nullable=False, unique=True)
|
||||
blacklisted = db.Column(db.Boolean, nullable=False, default=False)
|
||||
token = db.Column(db.String(32), nullable=True, default=None)
|
||||
|
||||
def __init__(self, email):
|
||||
self.email = email
|
||||
self.blacklisted = False
|
||||
self.token = None
|
||||
|
||||
|
||||
class NotificationType(enum.Enum):
|
||||
# Package / release / etc
|
||||
PACKAGE_EDIT = 1
|
||||
|
||||
# Approval review actions
|
||||
PACKAGE_APPROVAL = 2
|
||||
|
||||
# New thread
|
||||
NEW_THREAD = 3
|
||||
|
||||
# New Review
|
||||
NEW_REVIEW = 4
|
||||
|
||||
# Posted reply to subscribed thread
|
||||
THREAD_REPLY = 5
|
||||
|
||||
# A bot notification
|
||||
BOT = 6
|
||||
|
||||
# Added / removed as maintainer
|
||||
MAINTAINER = 7
|
||||
|
||||
# Editor misc
|
||||
EDITOR_ALERT = 8
|
||||
|
||||
# Editor misc
|
||||
EDITOR_MISC = 9
|
||||
|
||||
# Any other
|
||||
OTHER = 0
|
||||
|
||||
|
||||
def getTitle(self):
|
||||
return self.name.replace("_", " ").title()
|
||||
|
||||
def toName(self):
|
||||
return self.name.lower()
|
||||
|
||||
def get_description(self):
|
||||
if self == NotificationType.PACKAGE_EDIT:
|
||||
return "When another user edits your packages, releases, etc."
|
||||
elif self == NotificationType.PACKAGE_APPROVAL:
|
||||
return "Notifications from editors related to the package approval process."
|
||||
elif self == NotificationType.NEW_THREAD:
|
||||
return "When a thread is created on your package."
|
||||
elif self == NotificationType.NEW_REVIEW:
|
||||
return "When a user posts a review on your package."
|
||||
elif self == NotificationType.THREAD_REPLY:
|
||||
return "When someone replies to a thread you're watching."
|
||||
elif self == NotificationType.BOT:
|
||||
return "From a bot - for example, update notifications."
|
||||
elif self == NotificationType.MAINTAINER:
|
||||
return "When your package's maintainers change."
|
||||
elif self == NotificationType.EDITOR_ALERT:
|
||||
return "For editors: Important alerts."
|
||||
elif self == NotificationType.EDITOR_MISC:
|
||||
return "For editors: Minor notifications, including new threads."
|
||||
elif self == NotificationType.OTHER:
|
||||
return "Minor notifications not important enough for a dedicated category."
|
||||
else:
|
||||
return ""
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.value < other.value
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return [(choice, choice.getTitle()) for choice in cls]
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, item):
|
||||
return item if type(item) == NotificationType else NotificationType[item]
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="notifications")
|
||||
|
||||
causer_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
causer = db.relationship("User", foreign_keys=[causer_id], back_populates="caused_notifications")
|
||||
|
||||
type = db.Column(db.Enum(NotificationType), nullable=False, default=NotificationType.OTHER)
|
||||
|
||||
emailed = db.Column(db.Boolean(), nullable=False, default=False)
|
||||
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
url = db.Column(db.String(200), nullable=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id], back_populates="notifications")
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
||||
def __init__(self, user, causer, type, title, url, package=None):
|
||||
if len(title) > 100:
|
||||
title = title[:99] + "…"
|
||||
|
||||
self.user = user
|
||||
self.causer = causer
|
||||
self.type = type
|
||||
self.title = title
|
||||
self.url = url
|
||||
self.package = package
|
||||
|
||||
def can_send_email(self):
|
||||
prefs = self.user.notification_preferences
|
||||
return prefs and self.user.email and prefs.get_can_email(self.type)
|
||||
|
||||
def can_send_digest(self):
|
||||
prefs = self.user.notification_preferences
|
||||
return prefs and self.user.email and prefs.get_can_digest(self.type)
|
||||
|
||||
|
||||
class UserNotificationPreferences(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
user = db.relationship("User", back_populates="notification_preferences")
|
||||
|
||||
# 2 = immediate emails
|
||||
# 1 = daily digest emails
|
||||
# 0 = no emails
|
||||
|
||||
pref_package_edit = db.Column(db.Integer, nullable=False)
|
||||
pref_package_approval = db.Column(db.Integer, nullable=False)
|
||||
pref_new_thread = db.Column(db.Integer, nullable=False)
|
||||
pref_new_review = db.Column(db.Integer, nullable=False)
|
||||
pref_thread_reply = db.Column(db.Integer, nullable=False)
|
||||
pref_bot = db.Column(db.Integer, nullable=False)
|
||||
pref_maintainer = db.Column(db.Integer, nullable=False)
|
||||
pref_editor_alert = db.Column(db.Integer, nullable=False)
|
||||
pref_editor_misc = db.Column(db.Integer, nullable=False)
|
||||
pref_other = db.Column(db.Integer, nullable=False)
|
||||
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
self.pref_package_edit = 1
|
||||
self.pref_package_approval = 1
|
||||
self.pref_new_thread = 1
|
||||
self.pref_new_review = 1
|
||||
self.pref_thread_reply = 2
|
||||
self.pref_bot = 1
|
||||
self.pref_maintainer = 1
|
||||
self.pref_editor_alert = 1
|
||||
self.pref_editor_misc = 0
|
||||
self.pref_other = 0
|
||||
|
||||
def get_can_email(self, notification_type):
|
||||
return getattr(self, "pref_" + notification_type.toName()) == 2
|
||||
|
||||
def set_can_email(self, notification_type, value):
|
||||
value = 2 if value else 0
|
||||
setattr(self, "pref_" + notification_type.toName(), value)
|
||||
|
||||
def get_can_digest(self, notification_type):
|
||||
return getattr(self, "pref_" + notification_type.toName()) >= 1
|
||||
|
||||
def set_can_digest(self, notification_type, value):
|
||||
if self.get_can_email(notification_type):
|
||||
return
|
||||
|
||||
value = 1 if value else 0
|
||||
setattr(self, "pref_" + notification_type.toName(), value)
|
||||
BIN
app/public/static/bot_avatar.png
Normal file
BIN
app/public/static/bot_avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
app/public/static/contentdb_flag_blacklist.png
Normal file
BIN
app/public/static/contentdb_flag_blacklist.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
4
app/public/static/easymde.min.css
vendored
4
app/public/static/easymde.min.css
vendored
File diff suppressed because one or more lines are too long
11
app/public/static/easymde.min.js
vendored
11
app/public/static/easymde.min.js
vendored
File diff suppressed because one or more lines are too long
4450
app/public/static/fa/css/all.css
Normal file
4450
app/public/static/fa/css/all.css
Normal file
File diff suppressed because it is too large
Load Diff
5
app/public/static/fa/css/all.min.css
vendored
Normal file
5
app/public/static/fa/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
14
app/public/static/fa/css/brands.css
Normal file
14
app/public/static/fa/css/brands.css
Normal file
@@ -0,0 +1,14 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Brands';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-display: auto;
|
||||
src: url("../webfonts/fa-brands-400.eot");
|
||||
src: url("../webfonts/fa-brands-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.woff") format("woff"), url("../webfonts/fa-brands-400.ttf") format("truetype"), url("../webfonts/fa-brands-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.fab {
|
||||
font-family: 'Font Awesome 5 Brands'; }
|
||||
5
app/public/static/fa/css/brands.min.css
vendored
Normal file
5
app/public/static/fa/css/brands.min.css
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:normal;font-display:auto;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands"}
|
||||
4417
app/public/static/fa/css/fontawesome.css
vendored
Normal file
4417
app/public/static/fa/css/fontawesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
app/public/static/fa/css/fontawesome.min.css
vendored
Normal file
5
app/public/static/fa/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
app/public/static/fa/css/regular.css
Normal file
15
app/public/static/fa/css/regular.css
Normal file
@@ -0,0 +1,15 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: auto;
|
||||
src: url("../webfonts/fa-regular-400.eot");
|
||||
src: url("../webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.woff") format("woff"), url("../webfonts/fa-regular-400.ttf") format("truetype"), url("../webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.far {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400; }
|
||||
5
app/public/static/fa/css/regular.min.css
vendored
Normal file
5
app/public/static/fa/css/regular.min.css
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:400;font-display:auto;src:url(../webfonts/fa-regular-400.eot);src:url(../webfonts/fa-regular-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.woff) format("woff"),url(../webfonts/fa-regular-400.ttf) format("truetype"),url(../webfonts/fa-regular-400.svg#fontawesome) format("svg")}.far{font-family:"Font Awesome 5 Free";font-weight:400}
|
||||
16
app/public/static/fa/css/solid.css
Normal file
16
app/public/static/fa/css/solid.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: auto;
|
||||
src: url("../webfonts/fa-solid-900.eot");
|
||||
src: url("../webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.woff") format("woff"), url("../webfonts/fa-solid-900.ttf") format("truetype"), url("../webfonts/fa-solid-900.svg#fontawesome") format("svg"); }
|
||||
|
||||
.fa,
|
||||
.fas {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900; }
|
||||
5
app/public/static/fa/css/solid.min.css
vendored
Normal file
5
app/public/static/fa/css/solid.min.css
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}
|
||||
371
app/public/static/fa/css/svg-with-js.css
Normal file
371
app/public/static/fa/css/svg-with-js.css
Normal file
@@ -0,0 +1,371 @@
|
||||
/*!
|
||||
* Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
*/
|
||||
svg:not(:root).svg-inline--fa {
|
||||
overflow: visible; }
|
||||
|
||||
.svg-inline--fa {
|
||||
display: inline-block;
|
||||
font-size: inherit;
|
||||
height: 1em;
|
||||
overflow: visible;
|
||||
vertical-align: -.125em; }
|
||||
.svg-inline--fa.fa-lg {
|
||||
vertical-align: -.225em; }
|
||||
.svg-inline--fa.fa-w-1 {
|
||||
width: 0.0625em; }
|
||||
.svg-inline--fa.fa-w-2 {
|
||||
width: 0.125em; }
|
||||
.svg-inline--fa.fa-w-3 {
|
||||
width: 0.1875em; }
|
||||
.svg-inline--fa.fa-w-4 {
|
||||
width: 0.25em; }
|
||||
.svg-inline--fa.fa-w-5 {
|
||||
width: 0.3125em; }
|
||||
.svg-inline--fa.fa-w-6 {
|
||||
width: 0.375em; }
|
||||
.svg-inline--fa.fa-w-7 {
|
||||
width: 0.4375em; }
|
||||
.svg-inline--fa.fa-w-8 {
|
||||
width: 0.5em; }
|
||||
.svg-inline--fa.fa-w-9 {
|
||||
width: 0.5625em; }
|
||||
.svg-inline--fa.fa-w-10 {
|
||||
width: 0.625em; }
|
||||
.svg-inline--fa.fa-w-11 {
|
||||
width: 0.6875em; }
|
||||
.svg-inline--fa.fa-w-12 {
|
||||
width: 0.75em; }
|
||||
.svg-inline--fa.fa-w-13 {
|
||||
width: 0.8125em; }
|
||||
.svg-inline--fa.fa-w-14 {
|
||||
width: 0.875em; }
|
||||
.svg-inline--fa.fa-w-15 {
|
||||
width: 0.9375em; }
|
||||
.svg-inline--fa.fa-w-16 {
|
||||
width: 1em; }
|
||||
.svg-inline--fa.fa-w-17 {
|
||||
width: 1.0625em; }
|
||||
.svg-inline--fa.fa-w-18 {
|
||||
width: 1.125em; }
|
||||
.svg-inline--fa.fa-w-19 {
|
||||
width: 1.1875em; }
|
||||
.svg-inline--fa.fa-w-20 {
|
||||
width: 1.25em; }
|
||||
.svg-inline--fa.fa-pull-left {
|
||||
margin-right: .3em;
|
||||
width: auto; }
|
||||
.svg-inline--fa.fa-pull-right {
|
||||
margin-left: .3em;
|
||||
width: auto; }
|
||||
.svg-inline--fa.fa-border {
|
||||
height: 1.5em; }
|
||||
.svg-inline--fa.fa-li {
|
||||
width: 2em; }
|
||||
.svg-inline--fa.fa-fw {
|
||||
width: 1.25em; }
|
||||
|
||||
.fa-layers svg.svg-inline--fa {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0; }
|
||||
|
||||
.fa-layers {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: -.125em;
|
||||
width: 1em; }
|
||||
.fa-layers svg.svg-inline--fa {
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center; }
|
||||
|
||||
.fa-layers-text, .fa-layers-counter {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
text-align: center; }
|
||||
|
||||
.fa-layers-text {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center; }
|
||||
|
||||
.fa-layers-counter {
|
||||
background-color: #ff253a;
|
||||
border-radius: 1em;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
height: 1.5em;
|
||||
line-height: 1;
|
||||
max-width: 5em;
|
||||
min-width: 1.5em;
|
||||
overflow: hidden;
|
||||
padding: .25em;
|
||||
right: 0;
|
||||
text-overflow: ellipsis;
|
||||
top: 0;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right; }
|
||||
|
||||
.fa-layers-bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: bottom right;
|
||||
transform-origin: bottom right; }
|
||||
|
||||
.fa-layers-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: auto;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: bottom left;
|
||||
transform-origin: bottom left; }
|
||||
|
||||
.fa-layers-top-right {
|
||||
right: 0;
|
||||
top: 0;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right; }
|
||||
|
||||
.fa-layers-top-left {
|
||||
left: 0;
|
||||
right: auto;
|
||||
top: 0;
|
||||
-webkit-transform: scale(0.25);
|
||||
transform: scale(0.25);
|
||||
-webkit-transform-origin: top left;
|
||||
transform-origin: top left; }
|
||||
|
||||
.fa-lg {
|
||||
font-size: 1.33333em;
|
||||
line-height: 0.75em;
|
||||
vertical-align: -.0667em; }
|
||||
|
||||
.fa-xs {
|
||||
font-size: .75em; }
|
||||
|
||||
.fa-sm {
|
||||
font-size: .875em; }
|
||||
|
||||
.fa-1x {
|
||||
font-size: 1em; }
|
||||
|
||||
.fa-2x {
|
||||
font-size: 2em; }
|
||||
|
||||
.fa-3x {
|
||||
font-size: 3em; }
|
||||
|
||||
.fa-4x {
|
||||
font-size: 4em; }
|
||||
|
||||
.fa-5x {
|
||||
font-size: 5em; }
|
||||
|
||||
.fa-6x {
|
||||
font-size: 6em; }
|
||||
|
||||
.fa-7x {
|
||||
font-size: 7em; }
|
||||
|
||||
.fa-8x {
|
||||
font-size: 8em; }
|
||||
|
||||
.fa-9x {
|
||||
font-size: 9em; }
|
||||
|
||||
.fa-10x {
|
||||
font-size: 10em; }
|
||||
|
||||
.fa-fw {
|
||||
text-align: center;
|
||||
width: 1.25em; }
|
||||
|
||||
.fa-ul {
|
||||
list-style-type: none;
|
||||
margin-left: 2.5em;
|
||||
padding-left: 0; }
|
||||
.fa-ul > li {
|
||||
position: relative; }
|
||||
|
||||
.fa-li {
|
||||
left: -2em;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 2em;
|
||||
line-height: inherit; }
|
||||
|
||||
.fa-border {
|
||||
border: solid 0.08em #eee;
|
||||
border-radius: .1em;
|
||||
padding: .2em .25em .15em; }
|
||||
|
||||
.fa-pull-left {
|
||||
float: left; }
|
||||
|
||||
.fa-pull-right {
|
||||
float: right; }
|
||||
|
||||
.fa.fa-pull-left,
|
||||
.fas.fa-pull-left,
|
||||
.far.fa-pull-left,
|
||||
.fal.fa-pull-left,
|
||||
.fab.fa-pull-left {
|
||||
margin-right: .3em; }
|
||||
|
||||
.fa.fa-pull-right,
|
||||
.fas.fa-pull-right,
|
||||
.far.fa-pull-right,
|
||||
.fal.fa-pull-right,
|
||||
.fab.fa-pull-right {
|
||||
margin-left: .3em; }
|
||||
|
||||
.fa-spin {
|
||||
-webkit-animation: fa-spin 2s infinite linear;
|
||||
animation: fa-spin 2s infinite linear; }
|
||||
|
||||
.fa-pulse {
|
||||
-webkit-animation: fa-spin 1s infinite steps(8);
|
||||
animation: fa-spin 1s infinite steps(8); }
|
||||
|
||||
@-webkit-keyframes fa-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
@keyframes fa-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
.fa-rotate-90 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg); }
|
||||
|
||||
.fa-rotate-180 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg); }
|
||||
|
||||
.fa-rotate-270 {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg); }
|
||||
|
||||
.fa-flip-horizontal {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
|
||||
-webkit-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1); }
|
||||
|
||||
.fa-flip-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
|
||||
-webkit-transform: scale(1, -1);
|
||||
transform: scale(1, -1); }
|
||||
|
||||
.fa-flip-both, .fa-flip-horizontal.fa-flip-vertical {
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
|
||||
-webkit-transform: scale(-1, -1);
|
||||
transform: scale(-1, -1); }
|
||||
|
||||
:root .fa-rotate-90,
|
||||
:root .fa-rotate-180,
|
||||
:root .fa-rotate-270,
|
||||
:root .fa-flip-horizontal,
|
||||
:root .fa-flip-vertical,
|
||||
:root .fa-flip-both {
|
||||
-webkit-filter: none;
|
||||
filter: none; }
|
||||
|
||||
.fa-stack {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
width: 2.5em; }
|
||||
|
||||
.fa-stack-1x,
|
||||
.fa-stack-2x {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0; }
|
||||
|
||||
.svg-inline--fa.fa-stack-1x {
|
||||
height: 1em;
|
||||
width: 1.25em; }
|
||||
|
||||
.svg-inline--fa.fa-stack-2x {
|
||||
height: 2em;
|
||||
width: 2.5em; }
|
||||
|
||||
.fa-inverse {
|
||||
color: #fff; }
|
||||
|
||||
.sr-only {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px; }
|
||||
|
||||
.sr-only-focusable:active, .sr-only-focusable:focus {
|
||||
clip: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
width: auto; }
|
||||
|
||||
.svg-inline--fa .fa-primary {
|
||||
fill: var(--fa-primary-color, currentColor);
|
||||
opacity: 1;
|
||||
opacity: var(--fa-primary-opacity, 1); }
|
||||
|
||||
.svg-inline--fa .fa-secondary {
|
||||
fill: var(--fa-secondary-color, currentColor);
|
||||
opacity: 0.4;
|
||||
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||
|
||||
.svg-inline--fa.fa-swap-opacity .fa-primary {
|
||||
opacity: 0.4;
|
||||
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||
|
||||
.svg-inline--fa.fa-swap-opacity .fa-secondary {
|
||||
opacity: 1;
|
||||
opacity: var(--fa-primary-opacity, 1); }
|
||||
|
||||
.svg-inline--fa mask .fa-primary,
|
||||
.svg-inline--fa mask .fa-secondary {
|
||||
fill: black; }
|
||||
|
||||
.fad.fa-inverse {
|
||||
color: #fff; }
|
||||
5
app/public/static/fa/css/svg-with-js.min.css
vendored
Normal file
5
app/public/static/fa/css/svg-with-js.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2172
app/public/static/fa/css/v4-shims.css
Normal file
2172
app/public/static/fa/css/v4-shims.css
Normal file
File diff suppressed because it is too large
Load Diff
5
app/public/static/fa/css/v4-shims.min.css
vendored
Normal file
5
app/public/static/fa/css/v4-shims.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
app/public/static/fa/webfonts/fa-brands-400.eot
Normal file
BIN
app/public/static/fa/webfonts/fa-brands-400.eot
Normal file
Binary file not shown.
3535
app/public/static/fa/webfonts/fa-brands-400.svg
Normal file
3535
app/public/static/fa/webfonts/fa-brands-400.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 692 KiB |
BIN
app/public/static/fa/webfonts/fa-brands-400.ttf
Normal file
BIN
app/public/static/fa/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
app/public/static/fa/webfonts/fa-brands-400.woff
Normal file
BIN
app/public/static/fa/webfonts/fa-brands-400.woff
Normal file
Binary file not shown.
BIN
app/public/static/fa/webfonts/fa-brands-400.woff2
Normal file
BIN
app/public/static/fa/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
app/public/static/fa/webfonts/fa-regular-400.eot
Normal file
BIN
app/public/static/fa/webfonts/fa-regular-400.eot
Normal file
Binary file not shown.
803
app/public/static/fa/webfonts/fa-regular-400.svg
Normal file
803
app/public/static/fa/webfonts/fa-regular-400.svg
Normal file
@@ -0,0 +1,803 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!--
|
||||
Font Awesome Free 5.12.0 by @fontawesome - https://fontawesome.com
|
||||
License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
-->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<metadata>
|
||||
Created by FontForge 20190801 at Tue Dec 10 16:09:21 2019
|
||||
By Robert Madole
|
||||
Copyright (c) Font Awesome
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="FontAwesome5Free-Regular" horiz-adv-x="512" >
|
||||
<font-face
|
||||
font-family="Font Awesome 5 Free Regular"
|
||||
font-weight="400"
|
||||
font-stretch="normal"
|
||||
units-per-em="512"
|
||||
panose-1="2 0 5 3 0 0 0 0 0 0"
|
||||
ascent="448"
|
||||
descent="-64"
|
||||
bbox="-0.0663408 -64.0662 640.01 448.1"
|
||||
underline-thickness="25"
|
||||
underline-position="-50"
|
||||
unicode-range="U+0020-F5C8"
|
||||
/>
|
||||
<missing-glyph />
|
||||
<glyph glyph-name="heart" unicode=""
|
||||
d="M458.4 383.7c75.2998 -63.4004 64.0996 -166.601 10.5996 -221.3l-175.4 -178.7c-10 -10.2002 -23.2998 -15.7998 -37.5996 -15.7998c-14.2002 0 -27.5996 5.69922 -37.5996 15.8994l-175.4 178.7c-53.5996 54.7002 -64.5996 157.9 10.5996 221.2
|
||||
c57.8008 48.7002 147.101 41.2998 202.4 -15c55.2998 56.2998 144.6 63.5996 202.4 15zM434.8 196.2c36.2002 36.8994 43.7998 107.7 -7.2998 150.8c-38.7002 32.5996 -98.7002 27.9004 -136.5 -10.5996l-35 -35.7002l-35 35.7002
|
||||
c-37.5996 38.2998 -97.5996 43.1992 -136.5 10.5c-51.2002 -43.1006 -43.7998 -113.5 -7.2998 -150.7l175.399 -178.7c2.40039 -2.40039 4.40039 -2.40039 6.80078 0z" />
|
||||
<glyph glyph-name="star" unicode="" horiz-adv-x="576"
|
||||
d="M528.1 276.5c26.2002 -3.7998 36.7002 -36.0996 17.7002 -54.5996l-105.7 -103l25 -145.5c4.5 -26.3008 -23.1992 -45.9004 -46.3994 -33.7002l-130.7 68.7002l-130.7 -68.7002c-23.2002 -12.2998 -50.8994 7.39941 -46.3994 33.7002l25 145.5l-105.7 103
|
||||
c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2998l65.2998 132.4c11.7998 23.8994 45.7002 23.5996 57.4004 0l65.2998 -132.4zM388.6 135.7l100.601 98l-139 20.2002l-62.2002 126l-62.2002 -126l-139 -20.2002l100.601 -98l-23.7002 -138.4l124.3 65.2998
|
||||
l124.3 -65.2998z" />
|
||||
<glyph glyph-name="user" unicode="" horiz-adv-x="448"
|
||||
d="M313.6 144c74.2002 0 134.4 -60.2002 134.4 -134.4v-25.5996c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v25.5996c0 74.2002 60.2002 134.4 134.4 134.4c28.7998 0 42.5 -16 89.5996 -16s60.9004 16 89.5996 16zM400 -16v25.5996
|
||||
c0 47.6006 -38.7998 86.4004 -86.4004 86.4004c-14.6992 0 -37.8994 -16 -89.5996 -16c-51.2998 0 -75 16 -89.5996 16c-47.6006 0 -86.4004 -38.7998 -86.4004 -86.4004v-25.5996h352zM224 160c-79.5 0 -144 64.5 -144 144s64.5 144 144 144s144 -64.5 144 -144
|
||||
s-64.5 -144 -144 -144zM224 400c-52.9004 0 -96 -43.0996 -96 -96s43.0996 -96 96 -96s96 43.0996 96 96s-43.0996 96 -96 96z" />
|
||||
<glyph glyph-name="clock" unicode=""
|
||||
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM317.8 96.4004l-84.8994 61.6992
|
||||
c-3.10059 2.30078 -4.90039 5.90039 -4.90039 9.7002v164.2c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12v-141.7l66.7998 -48.5996c5.40039 -3.90039 6.5 -11.4004 2.60059 -16.7998l-18.8008 -25.9004c-3.89941 -5.2998 -11.3994 -6.5 -16.7998 -2.59961z
|
||||
" />
|
||||
<glyph glyph-name="list-alt" unicode=""
|
||||
d="M464 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h416zM458 16c3.31152 0 6 2.68848 6 6v340c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-340
|
||||
c0 -3.31152 2.68848 -6 6 -6h404zM416 108v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 204v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12
|
||||
v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM416 300v-24c0 -6.62695 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h200c6.62695 0 12 -5.37305 12 -12zM164 288c0 -19.8818 -16.1182 -36 -36 -36
|
||||
s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 192c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36zM164 96c0 -19.8818 -16.1182 -36 -36 -36s-36 16.1182 -36 36s16.1182 36 36 36s36 -16.1182 36 -36z" />
|
||||
<glyph glyph-name="flag" unicode=""
|
||||
d="M336.174 368c35.4668 0 73.0195 12.6914 108.922 28.1797c31.6406 13.6514 66.9043 -9.65723 66.9043 -44.1162v-239.919c0 -16.1953 -8.1543 -31.3057 -21.7129 -40.1631c-26.5762 -17.3643 -70.0693 -39.9814 -128.548 -39.9814c-68.6084 0 -112.781 32 -161.913 32
|
||||
c-56.5674 0 -89.957 -11.2803 -127.826 -28.5566v-83.4434c0 -8.83691 -7.16309 -16 -16 -16h-16c-8.83691 0 -16 7.16309 -16 16v406.438c-14.3428 8.2998 -24 23.7979 -24 41.5615c0 27.5693 23.2422 49.71 51.2012 47.8965
|
||||
c22.9658 -1.49023 41.8662 -19.4717 44.4805 -42.3379c0.177734 -1.52441 0.321289 -4.00781 0.321289 -5.54199c0 -4.30176 -1.10352 -11.1035 -2.46289 -15.1846c22.418 8.68555 49.4199 15.168 80.7207 15.168c68.6084 0 112.781 -32 161.913 -32zM464 112v240
|
||||
c-31.5059 -14.6338 -84.5547 -32 -127.826 -32c-59.9111 0 -101.968 32 -161.913 32c-41.4365 0 -80.4766 -16.5879 -102.261 -32v-232c31.4473 14.5967 84.4648 24 127.826 24c59.9111 0 101.968 -32 161.913 -32c41.4365 0 80.4775 16.5879 102.261 32z" />
|
||||
<glyph glyph-name="bookmark" unicode="" horiz-adv-x="384"
|
||||
d="M336 448c26.5098 0 48 -21.4902 48 -48v-464l-192 112l-192 -112v464c0 26.5098 21.4902 48 48 48h288zM336 19.5703v374.434c0 3.31348 -2.68555 5.99609 -6 5.99609h-276c-3.31152 0 -6 -2.68848 -6 -6v-374.43l144 84z" />
|
||||
<glyph glyph-name="image" unicode=""
|
||||
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM458 48c3.31152 0 6 2.68848 6 6v276c0 3.31152 -2.68848 6 -6 6h-404c-3.31152 0 -6 -2.68848 -6 -6v-276
|
||||
c0 -3.31152 2.68848 -6 6 -6h404zM128 296c22.0908 0 40 -17.9092 40 -40s-17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40zM96 96v48l39.5137 39.5146c4.6875 4.68652 12.2852 4.68652 16.9717 0l39.5146 -39.5146l119.514 119.515
|
||||
c4.6875 4.68652 12.2852 4.68652 16.9717 0l87.5146 -87.5146v-80h-320z" />
|
||||
<glyph glyph-name="edit" unicode="" horiz-adv-x="576"
|
||||
d="M402.3 103.1l32 32c5 5 13.7002 1.5 13.7002 -5.69922v-145.4c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h273.5c7.09961 0 10.7002 -8.59961 5.7002 -13.7002l-32 -32c-1.5 -1.5 -3.5 -2.2998 -5.7002 -2.2998h-241.5v-352h352
|
||||
v113.5c0 2.09961 0.799805 4.09961 2.2998 5.59961zM558.9 304.9l-262.601 -262.601l-90.3994 -10c-26.2002 -2.89941 -48.5 19.2002 -45.6006 45.6006l10 90.3994l262.601 262.601c22.8994 22.8994 59.8994 22.8994 82.6992 0l43.2002 -43.2002
|
||||
c22.9004 -22.9004 22.9004 -60 0.100586 -82.7998zM460.1 274l-58.0996 58.0996l-185.8 -185.899l-7.2998 -65.2998l65.2998 7.2998zM524.9 353.7l-43.2002 43.2002c-4.10059 4.09961 -10.7998 4.09961 -14.7998 0l-30.9004 -30.9004l58.0996 -58.0996l30.9004 30.8994
|
||||
c4 4.2002 4 10.7998 -0.0996094 14.9004z" />
|
||||
<glyph glyph-name="times-circle" unicode=""
|
||||
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM357.8 254.2l-62.2002 -62.2002l62.2002 -62.2002
|
||||
c4.7002 -4.7002 4.7002 -12.2998 0 -17l-22.5996 -22.5996c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-62.2002 62.2002l-62.2002 -62.2002c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-22.5996 22.5996c-4.7002 4.7002 -4.7002 12.2998 0 17l62.2002 62.2002l-62.2002 62.2002
|
||||
c-4.7002 4.7002 -4.7002 12.2998 0 17l22.5996 22.5996c4.7002 4.7002 12.2998 4.7002 17 0l62.2002 -62.2002l62.2002 62.2002c4.7002 4.7002 12.2998 4.7002 17 0l22.5996 -22.5996c4.7002 -4.7002 4.7002 -12.2998 0 -17z" />
|
||||
<glyph glyph-name="check-circle" unicode=""
|
||||
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200z
|
||||
M396.204 261.733c4.66699 -4.70508 4.63672 -12.3037 -0.0673828 -16.9717l-172.589 -171.204c-4.70508 -4.66797 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66797 4.70605 -4.63672 12.3047 0.0683594 16.9717l22.7188 22.5361
|
||||
c4.70508 4.66699 12.3027 4.63574 16.9697 -0.0693359l59.792 -60.2773l141.353 140.216c4.70508 4.66797 12.3027 4.6377 16.9697 -0.0673828z" />
|
||||
<glyph glyph-name="question-circle" unicode=""
|
||||
d="M256 440c136.957 0 248 -111.083 248 -248c0 -136.997 -111.043 -248 -248 -248s-248 111.003 -248 248c0 136.917 111.043 248 248 248zM256 -8c110.569 0 200 89.4697 200 200c0 110.529 -89.5088 200 -200 200c-110.528 0 -200 -89.5049 -200 -200
|
||||
c0 -110.569 89.4678 -200 200 -200zM363.244 247.2c0 -67.0518 -72.4209 -68.084 -72.4209 -92.8633v-6.33691c0 -6.62695 -5.37305 -12 -12 -12h-45.6475c-6.62695 0 -12 5.37305 -12 12v8.65918c0 35.7451 27.1006 50.0342 47.5791 61.5156
|
||||
c17.5615 9.84473 28.3242 16.541 28.3242 29.5791c0 17.2461 -21.999 28.6934 -39.7842 28.6934c-23.1885 0 -33.8936 -10.9775 -48.9424 -29.9697c-4.05664 -5.11914 -11.46 -6.07031 -16.666 -2.12402l-27.8232 21.0986
|
||||
c-5.10742 3.87207 -6.25098 11.0654 -2.64453 16.3633c23.627 34.6934 53.7217 54.1846 100.575 54.1846c49.0713 0 101.45 -38.3037 101.45 -88.7998zM298 80c0 -23.1592 -18.8408 -42 -42 -42s-42 18.8408 -42 42s18.8408 42 42 42s42 -18.8408 42 -42z" />
|
||||
<glyph glyph-name="eye" unicode="" horiz-adv-x="576"
|
||||
d="M288 304c0.0927734 0 0.244141 0.000976562 0.336914 0.000976562c61.6641 0 111.71 -50.0469 111.71 -111.711c0 -61.6631 -50.0459 -111.71 -111.71 -111.71s-111.71 50.0469 -111.71 111.71c0 8.71289 1.95898 22.5781 4.37305 30.9502
|
||||
c6.93066 -3.94141 19.0273 -7.18457 27 -7.24023c30.9121 0 56 25.0879 56 56c-0.0556641 7.97266 -3.29883 20.0693 -7.24023 27c8.42383 2.62207 22.4189 4.8623 31.2402 5zM572.52 206.6c1.9209 -3.79883 3.47949 -10.3379 3.47949 -14.5947
|
||||
s-1.55859 -10.7959 -3.47949 -14.5947c-54.1992 -105.771 -161.59 -177.41 -284.52 -177.41s-230.29 71.5898 -284.52 177.4c-1.9209 3.79883 -3.47949 10.3379 -3.47949 14.5947s1.55859 10.7959 3.47949 14.5947c54.1992 105.771 161.59 177.41 284.52 177.41
|
||||
s230.29 -71.5898 284.52 -177.4zM288 48c98.6602 0 189.1 55 237.93 144c-48.8398 89 -139.27 144 -237.93 144s-189.09 -55 -237.93 -144c48.8398 -89 139.279 -144 237.93 -144z" />
|
||||
<glyph glyph-name="eye-slash" unicode="" horiz-adv-x="640"
|
||||
d="M634 -23c3.31738 -2.65137 6.00977 -8.25098 6.00977 -12.498c0 -3.10449 -1.57715 -7.58984 -3.51953 -10.0117l-10 -12.4902c-2.65234 -3.31152 -8.24707 -6 -12.4902 -6c-3.09961 0 -7.58008 1.57227 -10 3.50977l-598 467.49
|
||||
c-3.31738 2.65137 -6.00977 8.25098 -6.00977 12.498c0 3.10449 1.57715 7.58984 3.51953 10.0117l10 12.4902c2.65234 3.31152 8.24707 6 12.4902 6c3.09961 0 7.58008 -1.57227 10 -3.50977zM296.79 301.53c6.33496 1.35059 16.7324 2.45801 23.21 2.46973
|
||||
c60.4805 0 109.36 -47.9102 111.58 -107.85zM343.21 82.46c-6.33496 -1.34375 -16.7334 -2.44629 -23.21 -2.45996c-60.4697 0 -109.35 47.9102 -111.58 107.84zM320 336c-19.8799 0 -39.2803 -2.7998 -58.2197 -7.09961l-46.4102 36.29
|
||||
c32.9199 11.8096 67.9297 18.8096 104.63 18.8096c122.93 0 230.29 -71.5898 284.57 -177.4c1.91992 -3.79883 3.47949 -10.3379 3.47949 -14.5947s-1.55957 -10.7959 -3.47949 -14.5947c-11.7197 -22.7598 -35.4189 -56.4092 -52.9004 -75.1104l-37.7402 29.5
|
||||
c14.333 15.0156 34.0449 41.9854 44 60.2002c-48.8398 89 -139.279 144 -237.93 144zM320 48c19.8896 0 39.2803 2.7998 58.2197 7.08984l46.4102 -36.2803c-32.9199 -11.7598 -67.9297 -18.8096 -104.63 -18.8096c-122.92 0 -230.28 71.5898 -284.51 177.4
|
||||
c-1.9209 3.79883 -3.47949 10.3379 -3.47949 14.5947s1.55859 10.7959 3.47949 14.5947c11.7168 22.7568 35.4111 56.4014 52.8896 75.1006l37.7402 -29.5c-14.3467 -15.0107 -34.0811 -41.9756 -44.0498 -60.1904c48.8496 -89 139.279 -144 237.93 -144z" />
|
||||
<glyph glyph-name="calendar-alt" unicode="" horiz-adv-x="448"
|
||||
d="M148 160h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM256 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
|
||||
c6.59961 0 12 -5.40039 12 -12v-40zM352 172c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM256 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40
|
||||
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM160 76c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM352 76c0 -6.59961 -5.40039 -12 -12 -12h-40
|
||||
c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40
|
||||
c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="comment" unicode=""
|
||||
d="M256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5c-3.80078 8.7998 -2 19 4.59961 26
|
||||
c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004
|
||||
l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
|
||||
<glyph glyph-name="folder" unicode=""
|
||||
d="M464 320c26.5098 0 48 -21.4902 48 -48v-224c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h146.74c8.49023 0 16.6299 -3.37012 22.6299 -9.37012l54.6299 -54.6299h192zM464 48v224h-198.62
|
||||
c-8.49023 0 -16.6299 3.37012 -22.6299 9.37012l-54.6299 54.6299h-140.12v-288h416z" />
|
||||
<glyph glyph-name="folder-open" unicode="" horiz-adv-x="576"
|
||||
d="M527.9 224c37.6992 0 60.6992 -41.5 40.6992 -73.4004l-79.8994 -128c-8.7998 -14.0996 -24.2002 -22.5996 -40.7002 -22.5996h-400c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h160l64 -64h160c26.5 0 48 -21.5 48 -48v-48h47.9004zM48 330v-233.4l62.9004 104.2
|
||||
c8.69922 14.4004 24.2998 23.2002 41.0996 23.2002h280v42c0 3.2998 -2.7002 6 -6 6h-173.9l-64 64h-134.1c-3.2998 0 -6 -2.7002 -6 -6zM448 48l80 128h-378.8l-77.2002 -128h376z" />
|
||||
<glyph glyph-name="chart-bar" unicode=""
|
||||
d="M396.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v230.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-230.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM204.8 96
|
||||
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v198.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-198.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM300.8 96
|
||||
c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v134.4c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-134.4c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004zM496 48c8.83984 0 16 -7.16016 16 -16v-16
|
||||
c0 -8.83984 -7.16016 -16 -16 -16h-464c-17.6699 0 -32 14.3301 -32 32v336c0 8.83984 7.16016 16 16 16h16c8.83984 0 16 -7.16016 16 -16v-320h448zM108.8 96c-6.39941 0 -12.7998 6.40039 -12.7998 12.7998v70.4004c0 6.39941 6.40039 12.7998 12.7998 12.7998h22.4004
|
||||
c6.39941 0 12.7998 -6.40039 12.7998 -12.7998v-70.4004c0 -6.39941 -6.40039 -12.7998 -12.7998 -12.7998h-22.4004z" />
|
||||
<glyph glyph-name="comments" unicode="" horiz-adv-x="576"
|
||||
d="M532 61.7998c15.2998 -30.7002 37.4004 -54.5 37.7998 -54.7998c6.2998 -6.7002 8 -16.5 4.40039 -25c-3.7002 -8.5 -12 -14 -21.2002 -14c-53.5996 0 -96.7002 20.2998 -125.2 38.7998c-19 -4.39941 -39 -6.7998 -59.7998 -6.7998
|
||||
c-86.2002 0 -159.9 40.4004 -191.3 97.7998c-9.7002 1.2002 -19.2002 2.7998 -28.4004 4.90039c-28.5 -18.6006 -71.7002 -38.7998 -125.2 -38.7998c-9.19922 0 -17.5996 5.5 -21.1992 14c-3.7002 8.5 -1.90039 18.2998 4.39941 25
|
||||
c0.400391 0.399414 22.4004 24.1992 37.7002 54.8994c-27.5 27.2002 -44 61.2002 -44 98.2002c0 88.4004 93.0996 160 208 160c86.2998 0 160.3 -40.5 191.8 -98.0996c99.7002 -11.8008 176.2 -77.9004 176.2 -157.9c0 -37.0996 -16.5 -71.0996 -44 -98.2002zM139.2 154.1
|
||||
l19.7998 -4.5c16 -3.69922 32.5 -5.59961 49 -5.59961c86.7002 0 160 51.2998 160 112s-73.2998 112 -160 112s-160 -51.2998 -160 -112c0 -28.7002 16.2002 -50.5996 29.7002 -64l24.7998 -24.5l-15.5 -31.0996c-2.59961 -5.10059 -5.2998 -10.1006 -8 -14.8008
|
||||
c14.5996 5.10059 29 12.3008 43.0996 21.4004zM498.3 96c13.5 13.4004 29.7002 35.2998 29.7002 64c0 49.2002 -48.2998 91.5 -112.7 106c0.299805 -3.2998 0.700195 -6.59961 0.700195 -10c0 -80.9004 -78 -147.5 -179.3 -158.3
|
||||
c29.0996 -29.6006 77.2998 -49.7002 131.3 -49.7002c16.5 0 33 1.90039 49 5.59961l19.9004 4.60059l17.0996 -11.1006c14.0996 -9.09961 28.5 -16.2998 43.0996 -21.3994c-2.69922 4.7002 -5.39941 9.7002 -8 14.7998l-15.5 31.0996z" />
|
||||
<glyph glyph-name="star-half" unicode="" horiz-adv-x="576"
|
||||
d="M288 62.7002v-54.2998l-130.7 -68.6006c-23.3994 -12.2998 -50.8994 7.60059 -46.3994 33.7002l25 145.5l-105.7 103c-19 18.5 -8.5 50.7998 17.7002 54.5996l146.1 21.2002l65.2998 132.4c5.90039 11.8994 17.2998 17.7998 28.7002 17.7998v-68.0996l-62.2002 -126
|
||||
l-139 -20.2002l100.601 -98l-23.7002 -138.4z" />
|
||||
<glyph glyph-name="lemon" unicode=""
|
||||
d="M484.112 420.111c28.1221 -28.123 35.9434 -68.0039 19.0215 -97.0547c-23.0576 -39.584 50.1436 -163.384 -82.3311 -295.86c-132.301 -132.298 -256.435 -59.3594 -295.857 -82.3291c-29.0459 -16.917 -68.9219 -9.11426 -97.0576 19.0205
|
||||
c-28.1221 28.1221 -35.9434 68.0029 -19.0215 97.0547c23.0566 39.5859 -50.1436 163.386 82.3301 295.86c132.308 132.309 256.407 59.3496 295.862 82.332c29.0498 16.9219 68.9307 9.09863 97.0537 -19.0234zM461.707 347.217
|
||||
c13.5166 23.2031 -27.7578 63.7314 -50.4883 50.4912c-66.6025 -38.7939 -165.646 45.5898 -286.081 -74.8457c-120.444 -120.445 -36.0449 -219.472 -74.8447 -286.08c-13.542 -23.2471 27.8145 -63.6953 50.4932 -50.4883
|
||||
c66.6006 38.7949 165.636 -45.5996 286.076 74.8428c120.444 120.445 36.0449 219.472 74.8447 286.08zM291.846 338.481c1.37012 -10.96 -6.40332 -20.957 -17.3643 -22.3271c-54.8467 -6.85547 -135.779 -87.7871 -142.636 -142.636
|
||||
c-1.37305 -10.9883 -11.3984 -18.7334 -22.3262 -17.3643c-10.9609 1.37012 -18.7344 11.3652 -17.3643 22.3262c9.16211 73.2852 104.167 168.215 177.364 177.364c10.9531 1.36816 20.9561 -6.40234 22.3262 -17.3633z" />
|
||||
<glyph glyph-name="credit-card" unicode="" horiz-adv-x="576"
|
||||
d="M527.9 416c26.5996 0 48.0996 -21.5 48.0996 -48v-352c0 -26.5 -21.5 -48 -48.0996 -48h-479.801c-26.5996 0 -48.0996 21.5 -48.0996 48v352c0 26.5 21.5 48 48.0996 48h479.801zM54.0996 368c-3.2998 0 -6 -2.7002 -6 -6v-42h479.801v42c0 3.2998 -2.7002 6 -6 6
|
||||
h-467.801zM521.9 16c3.2998 0 6 2.7002 6 6v170h-479.801v-170c0 -3.2998 2.7002 -6 6 -6h467.801zM192 116v-40c0 -6.59961 -5.40039 -12 -12 -12h-72c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h72c6.59961 0 12 -5.40039 12 -12zM384 116v-40
|
||||
c0 -6.59961 -5.40039 -12 -12 -12h-136c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h136c6.59961 0 12 -5.40039 12 -12z" />
|
||||
<glyph glyph-name="hdd" unicode="" horiz-adv-x="576"
|
||||
d="M567.403 212.358c5.59668 -8.04688 8.59668 -17.6113 8.59668 -27.4121v-136.946c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v136.946c0 8.30957 3.85156 20.5898 8.59668 27.4121l105.08 151.053
|
||||
c7.90625 11.3652 25.5596 20.5889 39.4033 20.5889h0.000976562h269.838h0.000976562c13.8438 0 31.4971 -9.22363 39.4033 -20.5889zM153.081 336l-77.9131 -112h425.664l-77.9131 112h-269.838zM528 48v128h-480v-128h480zM496 112c0 -17.6729 -14.3271 -32 -32 -32
|
||||
s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32zM400 112c0 -17.6729 -14.3271 -32 -32 -32s-32 14.3271 -32 32s14.3271 32 32 32s32 -14.3271 32 -32z" />
|
||||
<glyph glyph-name="hand-point-right" unicode=""
|
||||
d="M428.8 310.4c45.0996 0 83.2002 -38.1016 83.2002 -83.2002c0 -45.6162 -37.7646 -83.2002 -83.2002 -83.2002h-35.6475c-1.41602 -6.36719 -4.96875 -16.252 -7.92969 -22.0645c2.50586 -22.0059 -3.50293 -44.9775 -15.9844 -62.791
|
||||
c-1.14062 -52.4863 -37.3984 -91.1445 -99.9404 -91.1445h-21.2988c-60.0635 0 -98.5117 40 -127.2 40h-2.67871c-5.74707 -4.95215 -13.5361 -8 -22.1201 -8h-64c-17.6729 0 -32 12.8936 -32 28.7998v230.4c0 15.9062 14.3271 28.7998 32 28.7998h64.001
|
||||
c8.58398 0 16.373 -3.04785 22.1201 -8h2.67871c6.96387 0 14.8623 6.19336 30.1816 23.6689l0.128906 0.148438l0.130859 0.145508c8.85645 9.93652 18.1162 20.8398 25.8506 33.2529c18.7051 30.2471 30.3936 78.7842 75.707 78.7842c56.9277 0 92 -35.2861 92 -83.2002
|
||||
v-0.0839844c0 -6.21777 -0.974609 -16.2148 -2.17578 -22.3154h86.1768zM428.8 192c18.9756 0 35.2002 16.2246 35.2002 35.2002c0 18.7002 -16.7754 35.2002 -35.2002 35.2002h-158.399c0 17.3242 26.3994 35.1992 26.3994 70.3994c0 26.4004 -20.625 35.2002 -44 35.2002
|
||||
c-8.79395 0 -20.4443 -32.7119 -34.9258 -56.0996c-9.07422 -14.5752 -19.5244 -27.2256 -30.7988 -39.875c-16.1094 -18.374 -33.8359 -36.6328 -59.0752 -39.5967v-176.753c42.79 -3.7627 74.5088 -39.6758 120 -39.6758h21.2988
|
||||
c40.5244 0 57.124 22.1973 50.6006 61.3252c14.6113 8.00098 24.1514 33.9785 12.9248 53.625c19.3652 18.2246 17.7871 46.3809 4.9502 61.0498h91.0254zM88 64c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
|
||||
<glyph glyph-name="hand-point-left" unicode=""
|
||||
d="M0 227.2c0 45.0986 38.1006 83.2002 83.2002 83.2002h86.1758c-1.3623 6.91016 -2.17578 14.374 -2.17578 22.3994c0 47.9141 35.0723 83.2002 92 83.2002c45.3135 0 57.002 -48.5371 75.7061 -78.7852c7.73438 -12.4121 16.9951 -23.3154 25.8506 -33.2529
|
||||
l0.130859 -0.145508l0.128906 -0.148438c15.3213 -17.4746 23.2197 -23.668 30.1836 -23.668h2.67871c5.74707 4.95215 13.5361 8 22.1201 8h64c17.6729 0 32 -12.8936 32 -28.7998v-230.4c0 -15.9062 -14.3271 -28.7998 -32 -28.7998h-64
|
||||
c-8.58398 0 -16.373 3.04785 -22.1201 8h-2.67871c-28.6885 0 -67.1367 -40 -127.2 -40h-21.2988c-62.542 0 -98.8008 38.6582 -99.9404 91.1445c-12.4814 17.8135 -18.4922 40.7852 -15.9844 62.791c-2.96094 5.8125 -6.51367 15.6973 -7.92969 22.0645h-35.6465
|
||||
c-45.4355 0 -83.2002 37.584 -83.2002 83.2002zM48 227.2c0 -18.9756 16.2246 -35.2002 35.2002 -35.2002h91.0244c-12.8369 -14.6689 -14.415 -42.8252 4.9502 -61.0498c-11.2256 -19.6465 -1.68652 -45.624 12.9248 -53.625
|
||||
c-6.52246 -39.1279 10.0771 -61.3252 50.6016 -61.3252h21.2988c45.4912 0 77.21 35.9131 120 39.6768v176.752c-25.2393 2.96289 -42.9658 21.2227 -59.0752 39.5967c-11.2744 12.6494 -21.7246 25.2998 -30.7988 39.875
|
||||
c-14.4814 23.3877 -26.1318 56.0996 -34.9258 56.0996c-23.375 0 -44 -8.7998 -44 -35.2002c0 -35.2002 26.3994 -53.0752 26.3994 -70.3994h-158.399c-18.4248 0 -35.2002 -16.5 -35.2002 -35.2002zM448 88c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24
|
||||
s24 10.7451 24 24s-10.7451 24 -24 24z" />
|
||||
<glyph glyph-name="hand-point-up" unicode="" horiz-adv-x="448"
|
||||
d="M105.6 364.8c0 45.0996 38.1016 83.2002 83.2002 83.2002c45.6162 0 83.2002 -37.7646 83.2002 -83.2002v-35.6465c6.36719 -1.41602 16.252 -4.96875 22.0645 -7.92969c22.0059 2.50684 44.9775 -3.50293 62.791 -15.9844
|
||||
c52.4863 -1.14062 91.1445 -37.3984 91.1445 -99.9404v-21.2988c0 -60.0635 -40 -98.5117 -40 -127.2v-2.67871c4.95215 -5.74707 8 -13.5361 8 -22.1201v-64c0 -17.6729 -12.8936 -32 -28.7998 -32h-230.4c-15.9062 0 -28.7998 14.3271 -28.7998 32v64
|
||||
c0 8.58398 3.04785 16.373 8 22.1201v2.67871c0 6.96387 -6.19336 14.8623 -23.6689 30.1816l-0.148438 0.128906l-0.145508 0.130859c-9.93652 8.85645 -20.8398 18.1162 -33.2529 25.8506c-30.2471 18.7051 -78.7842 30.3936 -78.7842 75.707
|
||||
c0 56.9277 35.2861 92 83.2002 92h0.0839844c6.21777 0 16.2148 -0.974609 22.3154 -2.17578v86.1768zM224 364.8c0 18.9756 -16.2246 35.2002 -35.2002 35.2002c-18.7002 0 -35.2002 -16.7754 -35.2002 -35.2002v-158.399c-17.3242 0 -35.1992 26.3994 -70.3994 26.3994
|
||||
c-26.4004 0 -35.2002 -20.625 -35.2002 -44c0 -8.79395 32.7119 -20.4443 56.0996 -34.9258c14.5752 -9.07422 27.2256 -19.5244 39.875 -30.7988c18.374 -16.1094 36.6328 -33.8359 39.5967 -59.0752h176.753c3.7627 42.79 39.6758 74.5088 39.6758 120v21.2988
|
||||
c0 40.5244 -22.1973 57.124 -61.3252 50.6006c-8.00098 14.6113 -33.9785 24.1514 -53.625 12.9248c-18.2246 19.3652 -46.3809 17.7871 -61.0498 4.9502v91.0254zM352 24c-13.2549 0 -24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24z" />
|
||||
<glyph glyph-name="hand-point-down" unicode="" horiz-adv-x="448"
|
||||
d="M188.8 -64c-45.0986 0 -83.2002 38.1006 -83.2002 83.2002v86.1758c-6.91016 -1.3623 -14.374 -2.17578 -22.3994 -2.17578c-47.9141 0 -83.2002 35.0723 -83.2002 92c0 45.3135 48.5371 57.002 78.7852 75.707c12.4121 7.73438 23.3154 16.9951 33.2529 25.8506
|
||||
l0.145508 0.130859l0.148438 0.128906c17.4746 15.3213 23.668 23.2197 23.668 30.1836v2.67871c-4.95215 5.74707 -8 13.5361 -8 22.1201v64c0 17.6729 12.8936 32 28.7998 32h230.4c15.9062 0 28.7998 -14.3271 28.7998 -32v-64.001
|
||||
c0 -8.58398 -3.04785 -16.373 -8 -22.1201v-2.67871c0 -28.6885 40 -67.1367 40 -127.2v-21.2988c0 -62.542 -38.6582 -98.8008 -91.1445 -99.9404c-17.8135 -12.4814 -40.7852 -18.4922 -62.791 -15.9844c-5.8125 -2.96094 -15.6973 -6.51367 -22.0645 -7.92969v-35.6465
|
||||
c0 -45.4355 -37.584 -83.2002 -83.2002 -83.2002zM188.8 -16c18.9756 0 35.2002 16.2246 35.2002 35.2002v91.0244c14.6689 -12.8369 42.8252 -14.415 61.0498 4.9502c19.6465 -11.2256 45.624 -1.68652 53.625 12.9248c39.1279 -6.52246 61.3252 10.0771 61.3252 50.6016
|
||||
v21.2988c0 45.4912 -35.9131 77.21 -39.6768 120h-176.752c-2.96289 -25.2393 -21.2227 -42.9658 -39.5967 -59.0752c-12.6494 -11.2744 -25.2998 -21.7246 -39.875 -30.7988c-23.3877 -14.4814 -56.0996 -26.1318 -56.0996 -34.9258c0 -23.375 8.7998 -44 35.2002 -44
|
||||
c35.2002 0 53.0752 26.3994 70.3994 26.3994v-158.399c0 -18.4248 16.5 -35.2002 35.2002 -35.2002zM328 384c0 -13.2549 10.7451 -24 24 -24s24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24z" />
|
||||
<glyph glyph-name="copy" unicode="" horiz-adv-x="448"
|
||||
d="M433.941 382.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-80v-48c0 -26.5098 -21.4902 -48 -48 -48h-224c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h80v48c0 26.5098 21.4902 48 48 48
|
||||
h172.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM266 -16c3.31152 0 6 2.68848 6 6v42h-96c-26.5098 0 -48 21.4902 -48 48v224h-74c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM394 80c3.31152 0 6 2.68848 6 6v202h-88
|
||||
c-13.2549 0 -24 10.7451 -24 24v88h-106c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h212zM400 336v9.63184v0.000976562c0 1.37207 -0.787109 3.27246 -1.75684 4.24219l-48.3682 48.3682c-1.12598 1.125 -2.65234 1.75684 -4.24316 1.75684h-9.63184
|
||||
v-64h64z" />
|
||||
<glyph glyph-name="save" unicode="" horiz-adv-x="448"
|
||||
d="M433.941 318.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-268.118c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h268.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM272 368h-128v-80h128v80
|
||||
zM394 16c3.31152 0 6 2.68848 6 6v259.632v0.000976562c0 1.37207 -0.787109 3.27246 -1.75684 4.24219l-78.2432 78.2432v-100.118c0 -13.2549 -10.7451 -24 -24 -24h-176c-13.2549 0 -24 10.7451 -24 24v104h-42c-3.31152 0 -6 -2.68848 -6 -6v-340
|
||||
c0 -3.31152 2.68848 -6 6 -6h340zM224 216c48.5234 0 88 -39.4766 88 -88s-39.4766 -88 -88 -88s-88 39.4766 -88 88s39.4766 88 88 88zM224 88c22.0557 0 40 17.9443 40 40s-17.9443 40 -40 40s-40 -17.9443 -40 -40s17.9443 -40 40 -40z" />
|
||||
<glyph glyph-name="square" unicode="" horiz-adv-x="448"
|
||||
d="M400 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352zM394 16c3.2998 0 6 2.7002 6 6v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340z" />
|
||||
<glyph glyph-name="envelope" unicode=""
|
||||
d="M464 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h416zM464 336h-416v-40.8047c22.4248 -18.2627 58.1797 -46.6602 134.587 -106.49
|
||||
c16.834 -13.2422 50.2051 -45.0762 73.4131 -44.7012c23.2119 -0.371094 56.5723 31.4541 73.4131 44.7012c76.4189 59.8389 112.165 88.2305 134.587 106.49v40.8047zM48 48h416v185.601c-22.915 -18.252 -55.4189 -43.8691 -104.947 -82.6523
|
||||
c-22.5439 -17.748 -60.3359 -55.1787 -103.053 -54.9473c-42.9277 -0.231445 -81.2051 37.75 -103.062 54.9551c-49.5293 38.7842 -82.0244 64.3945 -104.938 82.6455v-185.602z" />
|
||||
<glyph glyph-name="lightbulb" unicode="" horiz-adv-x="352"
|
||||
d="M176 368c8.83984 0 16 -7.16016 16 -16s-7.16016 -16 -16 -16c-35.2803 0 -64 -28.7002 -64 -64c0 -8.83984 -7.16016 -16 -16 -16s-16 7.16016 -16 16c0 52.9404 43.0596 96 96 96zM96.0596 -11.1699l-0.0400391 43.1797h159.961l-0.0507812 -43.1797
|
||||
c-0.00976562 -3.13965 -0.939453 -6.21973 -2.67969 -8.83984l-24.5098 -36.8398c-2.95996 -4.45996 -7.95996 -7.14062 -13.3203 -7.14062h-78.8496c-5.35059 0 -10.3506 2.68066 -13.3203 7.14062l-24.5098 36.8398c-1.75 2.62012 -2.68066 5.68945 -2.68066 8.83984z
|
||||
M176 448c97.2002 0 176 -78.7998 176 -176c0 -44.3701 -16.4502 -84.8496 -43.5498 -115.79c-16.6406 -18.9795 -42.7402 -58.79 -52.4199 -92.1602v-0.0498047h-48v0.0996094c0.00390625 4.04199 0.999023 10.4482 2.21973 14.3008
|
||||
c5.67969 17.9893 22.9902 64.8496 62.0996 109.46c20.4102 23.29 31.6504 53.1699 31.6504 84.1396c0 70.5801 -57.4199 128 -128 128c-68.2803 0 -128.15 -54.3604 -127.95 -128c0.0898438 -30.9902 11.0703 -60.71 31.6104 -84.1396
|
||||
c39.3496 -44.9004 56.5801 -91.8604 62.1699 -109.67c1.42969 -4.56055 2.13965 -9.30078 2.15039 -14.0703v-0.120117h-48v0.0595703c-9.68066 33.3604 -35.7803 73.1709 -52.4209 92.1602c-27.1094 30.9307 -43.5596 71.4102 -43.5596 115.78
|
||||
c0 93.0303 73.7197 176 176 176z" />
|
||||
<glyph glyph-name="bell" unicode="" horiz-adv-x="448"
|
||||
d="M439.39 85.71c6 -6.44043 8.66016 -14.1602 8.61035 -21.71c-0.0996094 -16.4004 -12.9805 -32 -32.0996 -32h-383.801c-19.1191 0 -31.9893 15.5996 -32.0996 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c19.3193 20.7598 55.4697 51.9902 55.4697 154.29
|
||||
c0 77.7002 54.4795 139.9 127.939 155.16v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -102.3 36.1504 -133.53 55.4697 -154.29zM67.5303 80h312.939
|
||||
c-21.2197 27.96 -44.4199 74.3203 -44.5293 159.42c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112s-112 -50.1396 -112 -112c0 -0.200195 0.0595703 -0.379883 0.0595703 -0.580078
|
||||
c-0.109375 -85.0898 -23.3096 -131.45 -44.5293 -159.42zM224 -64c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
|
||||
<glyph glyph-name="hospital" unicode="" horiz-adv-x="448"
|
||||
d="M128 204v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12zM268 192c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40
|
||||
c0 -6.62695 -5.37305 -12 -12 -12h-40zM192 108c0 -6.62695 -5.37305 -12 -12 -12h-40c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-40zM268 96c-6.62695 0 -12 5.37305 -12 12v40c0 6.62695 5.37305 12 12 12h40
|
||||
c6.62695 0 12 -5.37305 12 -12v-40c0 -6.62695 -5.37305 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.62695 5.37305 12 12 12h19.5v378.965c0 11.6172 10.7451 21.0352 24 21.0352h88.5v40c0 13.2549 10.7451 24 24 24h112c13.2549 0 24 -10.7451 24 -24v-40h88.5
|
||||
c13.2549 0 24 -9.41797 24 -21.0352v-378.965h19.5c6.62695 0 12 -5.37305 12 -12zM79.5 -15h112.5v67c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-67h112.5v351h-64.5v-24c0 -13.2549 -10.7451 -24 -24 -24h-112c-13.2549 0 -24 10.7451 -24 24v24
|
||||
h-64.5v-351zM266 384h-26v26c0 3.31152 -2.68848 6 -6 6h-20c-3.31152 0 -6 -2.68848 -6 -6v-26h-26c-3.31152 0 -6 -2.68848 -6 -6v-20c0 -3.31152 2.68848 -6 6 -6h26v-26c0 -3.31152 2.68848 -6 6 -6h20c3.31152 0 6 2.68848 6 6v26h26c3.31152 0 6 2.68848 6 6v20
|
||||
c0 3.31152 -2.68848 6 -6 6z" />
|
||||
<glyph glyph-name="plus-square" unicode="" horiz-adv-x="448"
|
||||
d="M352 208v-32c0 -6.59961 -5.40039 -12 -12 -12h-88v-88c0 -6.59961 -5.40039 -12 -12 -12h-32c-6.59961 0 -12 5.40039 -12 12v88h-88c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h88v88c0 6.59961 5.40039 12 12 12h32c6.59961 0 12 -5.40039 12 -12
|
||||
v-88h88c6.59961 0 12 -5.40039 12 -12zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340
|
||||
c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="circle" unicode=""
|
||||
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200z" />
|
||||
<glyph glyph-name="smile" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
|
||||
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM332 135.4c8.5 10.1992 23.7002 11.5 33.7998 3.09961c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998
|
||||
c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.0996 8.40039 25.2998 7.09961 33.7998 -3.09961c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004z" />
|
||||
<glyph glyph-name="frown" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
|
||||
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 144c40.2002 0 78 -17.7002 103.8 -48.5996c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008
|
||||
c-10.7002 -8.7998 -25.7002 -6.59961 -33.7998 3.10059c-16.6006 20 -41 31.3994 -66.9004 31.3994s-50.2998 -11.5 -66.9004 -31.3994c-8.5 -10.2002 -23.5996 -11.5 -33.7998 -3.10059c-10.2002 8.5 -11.5996 23.6006 -3.09961 33.8008
|
||||
c25.7998 30.8994 63.5996 48.5996 103.8 48.5996z" />
|
||||
<glyph glyph-name="meh" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
|
||||
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM336 128c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-176c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h176z
|
||||
" />
|
||||
<glyph glyph-name="keyboard" unicode="" horiz-adv-x="576"
|
||||
d="M528 384c26.5098 0 48 -21.4902 48 -48v-288c0 -26.5098 -21.4902 -48 -48 -48h-480c-26.5098 0 -48 21.4902 -48 48v288c0 26.5098 21.4902 48 48 48h480zM536 48v288c0 4.41113 -3.58887 8 -8 8h-480c-4.41113 0 -8 -3.58887 -8 -8v-288c0 -4.41113 3.58887 -8 8 -8
|
||||
h480c4.41113 0 8 3.58887 8 8zM170 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM266 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
|
||||
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM362 178c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM458 178c0 -6.62695 -5.37305 -12 -12 -12h-28
|
||||
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 96c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 96
|
||||
c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM122 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28
|
||||
c6.62695 0 12 -5.37305 12 -12v-28zM218 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM314 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28
|
||||
c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM410 260c0 -6.62695 -5.37305 -12 -12 -12h-28c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM506 260c0 -6.62695 -5.37305 -12 -12 -12h-28
|
||||
c-6.62695 0 -12 5.37305 -12 12v28c0 6.62695 5.37305 12 12 12h28c6.62695 0 12 -5.37305 12 -12v-28zM408 102c0 -6.62695 -5.37305 -12 -12 -12h-216c-6.62695 0 -12 5.37305 -12 12v16c0 6.62695 5.37305 12 12 12h216c6.62695 0 12 -5.37305 12 -12v-16z" />
|
||||
<glyph glyph-name="calendar" unicode="" horiz-adv-x="448"
|
||||
d="M400 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12
|
||||
v-52h48zM394 -16c3.2998 0 6 2.7002 6 6v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340z" />
|
||||
<glyph glyph-name="play-circle" unicode=""
|
||||
d="M371.7 210c16.3994 -9.2002 16.3994 -32.9004 0 -42l-176 -101c-15.9004 -8.7998 -35.7002 2.59961 -35.7002 21v208c0 18.5 19.9004 29.7998 35.7002 21zM504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192
|
||||
c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200z" />
|
||||
<glyph glyph-name="minus-square" unicode="" horiz-adv-x="448"
|
||||
d="M108 164c-6.59961 0 -12 5.40039 -12 12v32c0 6.59961 5.40039 12 12 12h232c6.59961 0 12 -5.40039 12 -12v-32c0 -6.59961 -5.40039 -12 -12 -12h-232zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h352
|
||||
c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="check-square" unicode="" horiz-adv-x="448"
|
||||
d="M400 416c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h352zM400 16v352h-352v-352h352zM364.136 257.724l-172.589 -171.204
|
||||
c-4.70508 -4.66699 -12.3027 -4.63672 -16.9697 0.0683594l-90.7812 91.5156c-4.66699 4.70508 -4.63672 12.3037 0.0693359 16.9717l22.7188 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l59.792 -60.2773l141.353 140.217
|
||||
c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0683594l22.5361 -22.7178c4.66699 -4.70605 4.63672 -12.3047 -0.0683594 -16.9717z" />
|
||||
<glyph glyph-name="share-square" unicode="" horiz-adv-x="576"
|
||||
d="M561.938 289.94c18.75 -18.7402 18.75 -49.1406 0 -67.8809l-143.998 -144c-29.9727 -29.9727 -81.9404 -9.05273 -81.9404 33.9404v53.7998c-101.266 -7.83691 -99.625 -31.6406 -84.1104 -78.7598c14.2285 -43.0889 -33.4736 -79.248 -71.0195 -55.7402
|
||||
c-51.6924 32.3057 -84.8701 83.0635 -84.8701 144.76c0 39.3408 12.2197 72.7402 36.3301 99.3008c19.8398 21.8398 47.7402 38.4697 82.9102 49.4199c36.7295 11.4395 78.3096 16.1094 120.76 17.9893v57.1982c0 42.9355 51.9258 63.9541 81.9404 33.9404zM384 112l144 144
|
||||
l-144 144v-104.09c-110.86 -0.90332 -240 -10.5166 -240 -119.851c0 -52.1396 32.79 -85.6094 62.3096 -104.06c-39.8174 120.65 48.999 141.918 177.69 143.84v-103.84zM408.74 27.5068c6.14844 1.75684 15.5449 5.92383 20.9736 9.30273
|
||||
c7.97656 4.95215 18.2861 -0.825195 18.2861 -10.2139v-42.5957c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h132c6.62695 0 12 -5.37305 12 -12v-4.48633c0 -4.91699 -2.9873 -9.36914 -7.56934 -11.1514
|
||||
c-13.7021 -5.33105 -26.3955 -11.5371 -38.0498 -18.585c-1.59668 -0.974609 -4.41016 -1.77051 -6.28027 -1.77734h-86.1006c-3.31152 0 -6 -2.68848 -6 -6v-340c0 -3.31152 2.68848 -6 6 -6h340c3.31152 0 6 2.68848 6 6v25.9658c0 5.37012 3.5791 10.0596 8.74023 11.541
|
||||
z" />
|
||||
<glyph glyph-name="compass" unicode="" horiz-adv-x="496"
|
||||
d="M347.94 318.14c16.6592 7.61035 33.8096 -9.54004 26.1992 -26.1992l-65.9697 -144.341c-2.73047 -5.97363 -9.7959 -13.0391 -15.7695 -15.7695l-144.341 -65.9697c-16.6592 -7.61035 -33.8096 9.5498 -26.1992 26.1992l65.9697 144.341
|
||||
c2.73047 5.97363 9.7959 13.0391 15.7695 15.7695zM270.58 169.42c12.4697 12.4697 12.4697 32.6904 0 45.1602s-32.6904 12.4697 -45.1602 0s-12.4697 -32.6904 0 -45.1602s32.6904 -12.4697 45.1602 0zM248 440c136.97 0 248 -111.03 248 -248s-111.03 -248 -248 -248
|
||||
s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
|
||||
<glyph glyph-name="caret-square-down" unicode="" horiz-adv-x="448"
|
||||
d="M125.1 240h197.801c10.6992 0 16.0996 -13 8.5 -20.5l-98.9004 -98.2998c-4.7002 -4.7002 -12.2002 -4.7002 -16.9004 0l-98.8994 98.2998c-7.7002 7.5 -2.2998 20.5 8.39941 20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
|
||||
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="caret-square-up" unicode="" horiz-adv-x="448"
|
||||
d="M322.9 144h-197.801c-10.6992 0 -16.0996 13 -8.5 20.5l98.9004 98.2998c4.7002 4.7002 12.2002 4.7002 16.9004 0l98.8994 -98.2998c7.7002 -7.5 2.2998 -20.5 -8.39941 -20.5zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
|
||||
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="caret-square-right" unicode="" horiz-adv-x="448"
|
||||
d="M176 93.0996v197.801c0 10.6992 13 16.0996 20.5 8.5l98.2998 -98.9004c4.7002 -4.7002 4.7002 -12.2002 0 -16.9004l-98.2998 -98.8994c-7.5 -7.7002 -20.5 -2.2998 -20.5 8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
|
||||
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="file" unicode="" horiz-adv-x="384"
|
||||
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
|
||||
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416z" />
|
||||
<glyph glyph-name="file-alt" unicode="" horiz-adv-x="384"
|
||||
d="M288 200v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v28c0 6.59961 5.40039 12 12 12h168c6.59961 0 12 -5.40039 12 -12zM276 128c6.59961 0 12 -5.40039 12 -12v-28c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12
|
||||
v28c0 6.59961 5.40039 12 12 12h168zM384 316.1v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996l83.9004 -83.9004c9 -8.90039 14.0996 -21.2002 14.0996 -33.9004z
|
||||
M256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288z" />
|
||||
<glyph glyph-name="thumbs-up" unicode=""
|
||||
d="M466.27 161.31c4.6748 -22.6465 0.864258 -44.5371 -8.98926 -62.9893c2.95898 -23.8682 -4.02148 -48.5654 -17.3398 -66.9902c-0.954102 -55.9072 -35.8232 -95.3301 -112.94 -95.3301c-7 0 -15 0.00976562 -22.2197 0.00976562
|
||||
c-102.742 0 -133.293 38.9395 -177.803 39.9404c-3.56934 -13.7764 -16.085 -23.9502 -30.9775 -23.9502h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h98.7598c19.1455 16.9531 46.0137 60.6533 68.7598 83.4004
|
||||
c13.667 13.667 10.1533 108.6 71.7607 108.6c57.5801 0 95.2695 -31.9355 95.2695 -104.73c0 -18.4092 -3.92969 -33.7295 -8.84961 -46.5391h36.4795c48.6025 0 85.8203 -41.5654 85.8203 -85.5801c0 -19.1504 -4.95996 -34.9902 -13.7305 -49.8408zM404.52 107.48
|
||||
c21.5811 20.3838 18.6992 51.0645 5.21094 65.6191c9.44922 0 22.3594 18.9102 22.2695 37.8105c-0.0898438 18.9102 -16.71 37.8203 -37.8203 37.8203h-103.989c0 37.8193 28.3594 55.3691 28.3594 94.5391c0 23.75 0 56.7305 -47.2695 56.7305
|
||||
c-18.9102 -18.9102 -9.45996 -66.1797 -37.8203 -94.54c-26.5596 -26.5703 -66.1797 -97.46 -94.54 -97.46h-10.9199v-186.17c53.6113 0 100.001 -37.8203 171.64 -37.8203h37.8203c35.5117 0 60.8203 17.1201 53.1201 65.9004
|
||||
c15.2002 8.16016 26.5 36.4395 13.9395 57.5703zM88 16c0 13.2549 -10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24s24 10.7451 24 24z" />
|
||||
<glyph glyph-name="thumbs-down" unicode=""
|
||||
d="M466.27 222.69c8.77051 -14.8506 13.7305 -30.6904 13.7305 -49.8408c0 -44.0146 -37.2178 -85.5801 -85.8203 -85.5801h-36.4795c4.91992 -12.8096 8.84961 -28.1299 8.84961 -46.5391c0 -72.7949 -37.6895 -104.73 -95.2695 -104.73
|
||||
c-61.6074 0 -58.0938 94.9326 -71.7607 108.6c-22.7461 22.7471 -49.6133 66.4473 -68.7598 83.4004h-7.05176c-5.5332 -9.56152 -15.8662 -16 -27.708 -16h-64c-17.6729 0 -32 14.3271 -32 32v240c0 17.6729 14.3271 32 32 32h64c8.11328 0 15.5146 -3.02539 21.1553 -8
|
||||
h10.8447c40.9971 0 73.1953 39.9902 176.78 39.9902c7.21973 0 15.2197 0.00976562 22.2197 0.00976562c77.1172 0 111.986 -39.4229 112.94 -95.3301c13.3184 -18.4248 20.2979 -43.1221 17.3398 -66.9902c9.85352 -18.4521 13.6641 -40.3428 8.98926 -62.9893zM64 152
|
||||
c13.2549 0 24 10.7451 24 24s-10.7451 24 -24 24s-24 -10.7451 -24 -24s10.7451 -24 24 -24zM394.18 135.27c21.1104 0 37.7305 18.9102 37.8203 37.8203c0.0898438 18.9004 -12.8203 37.8105 -22.2695 37.8105c13.4883 14.5547 16.3701 45.2354 -5.21094 65.6191
|
||||
c12.5605 21.1309 1.26074 49.4102 -13.9395 57.5703c7.7002 48.7803 -17.6084 65.9004 -53.1201 65.9004h-37.8203c-71.6387 0 -118.028 -37.8203 -171.64 -37.8203v-186.17h10.9199c28.3604 0 67.9805 -70.8896 94.54 -97.46
|
||||
c28.3604 -28.3604 18.9102 -75.6299 37.8203 -94.54c47.2695 0 47.2695 32.9805 47.2695 56.7305c0 39.1699 -28.3594 56.7197 -28.3594 94.5391h103.989z" />
|
||||
<glyph glyph-name="sun" unicode=""
|
||||
d="M494.2 226.1c11.2002 -7.59961 17.7998 -20.0996 17.8994 -33.6992c0 -13.4004 -6.69922 -26 -17.7998 -33.5l-59.7998 -40.5l13.7002 -71c2.5 -13.2002 -1.60059 -26.8008 -11.1006 -36.3008s-22.8994 -13.7998 -36.2998 -11.0996l-70.8994 13.7002l-40.4004 -59.9004
|
||||
c-7.5 -11.0996 -20.0996 -17.7998 -33.5 -17.7998s-26 6.7002 -33.5 17.9004l-40.4004 59.8994l-70.7998 -13.7002c-13.3994 -2.59961 -26.7998 1.60059 -36.2998 11.1006s-13.7002 23.0996 -11.0996 36.2998l13.6992 71l-59.7998 40.5
|
||||
c-11.0996 7.5 -17.7998 20 -17.7998 33.5s6.59961 26 17.7998 33.5996l59.7998 40.5l-13.6992 71c-2.60059 13.2002 1.59961 26.7002 11.0996 36.3008c9.5 9.59961 23 13.6992 36.2998 11.1992l70.7998 -13.6992l40.4004 59.8994c15.0996 22.2998 51.9004 22.2998 67 0
|
||||
l40.4004 -59.8994l70.8994 13.6992c13 2.60059 26.6006 -1.59961 36.2002 -11.0996c9.5 -9.59961 13.7002 -23.2002 11.0996 -36.4004l-13.6992 -71zM381.3 140.5l76.7998 52.0996l-76.7998 52l17.6006 91.1006l-91 -17.6006l-51.9004 76.9004l-51.7998 -76.7998
|
||||
l-91 17.5996l17.5996 -91.2002l-76.7998 -52l76.7998 -52l-17.5996 -91.1992l90.8994 17.5996l51.9004 -77l51.9004 76.9004l91 -17.6006zM256 296c57.2998 0 104 -46.7002 104 -104s-46.7002 -104 -104 -104s-104 46.7002 -104 104s46.7002 104 104 104zM256 136
|
||||
c30.9004 0 56 25.0996 56 56s-25.0996 56 -56 56s-56 -25.0996 -56 -56s25.0996 -56 56 -56z" />
|
||||
<glyph glyph-name="moon" unicode=""
|
||||
d="M279.135 -64c-141.424 0 -256 114.64 -256 256c0 141.425 114.641 256 256 256c13.0068 -0.00195312 33.9443 -1.91797 46.7354 -4.27734c44.0205 -8.13086 53.7666 -66.8691 15.0215 -88.9189c-41.374 -23.5439 -67.4336 -67.4121 -67.4336 -115.836
|
||||
c0 -83.5234 75.9238 -146.475 158.272 -130.792c43.6904 8.32129 74.5186 -42.5693 46.248 -77.4004c-47.8613 -58.9717 -120.088 -94.7754 -198.844 -94.7754zM279.135 400c-114.875 0 -208 -93.125 -208 -208s93.125 -208 208 -208
|
||||
c65.2314 0 123.439 30.0361 161.575 77.0244c-111.611 -21.2568 -215.252 64.0957 -215.252 177.943c0 67.5127 36.9326 126.392 91.6934 157.555c-12.3271 2.27637 -25.0312 3.47754 -38.0166 3.47754z" />
|
||||
<glyph glyph-name="caret-square-left" unicode="" horiz-adv-x="448"
|
||||
d="M272 290.9v-197.801c0 -10.6992 -13 -16.0996 -20.5 -8.5l-98.2998 98.9004c-4.7002 4.7002 -4.7002 12.2002 0 16.9004l98.2998 98.8994c7.5 7.7002 20.5 2.2998 20.5 -8.39941zM448 368v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352
|
||||
c0 26.5 21.5 48 48 48h352c26.5 0 48 -21.5 48 -48zM400 22v340c0 3.2998 -2.7002 6 -6 6h-340c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="dot-circle" unicode=""
|
||||
d="M256 392c-110.549 0 -200 -89.4678 -200 -200c0 -110.549 89.4678 -200 200 -200c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200zM256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248z
|
||||
M256 272c44.1826 0 80 -35.8174 80 -80s-35.8174 -80 -80 -80s-80 35.8174 -80 80s35.8174 80 80 80z" />
|
||||
<glyph glyph-name="building" unicode="" horiz-adv-x="448"
|
||||
d="M128 300v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12zM268 288c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40
|
||||
c0 -6.59961 -5.40039 -12 -12 -12h-40zM140 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM268 192c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40
|
||||
c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM192 108c0 -6.59961 -5.40039 -12 -12 -12h-40c-6.59961 0 -12 5.40039 -12 12v40c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40zM268 96c-6.59961 0 -12 5.40039 -12 12v40
|
||||
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-40zM448 -28v-36h-448v36c0 6.59961 5.40039 12 12 12h19.5v440c0 13.2998 10.7002 24 24 24h337c13.2998 0 24 -10.7002 24 -24v-440h19.5
|
||||
c6.59961 0 12 -5.40039 12 -12zM79.5 -15h112.5v67c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-67h112.5v414l-288.5 1z" />
|
||||
<glyph glyph-name="file-pdf" unicode="" horiz-adv-x="384"
|
||||
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
|
||||
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM298.2 127.7c10.5 -10.5 8 -38.7002 -17.5 -38.7002c-14.7998 0 -36.9004 6.7998 -55.7998 17c-21.6006 -3.59961 -46 -12.7002 -68.4004 -20.0996c-50.0996 -86.4004 -79.4004 -47 -76.0996 -31.2002
|
||||
c4 20 31 35.8994 51 46.2002c10.5 18.3994 25.3994 50.5 35.3994 74.3994c-7.39941 28.6006 -11.3994 51 -7 67.1006c4.7998 17.6992 38.4004 20.2998 42.6006 -5.90039c4.69922 -15.4004 -1.5 -39.9004 -5.40039 -56c8.09961 -21.2998 19.5996 -35.7998 36.7998 -46.2998
|
||||
c17.4004 2.2002 52.2002 5.5 64.4004 -6.5zM100.1 49.9004c0 -0.700195 11.4004 4.69922 30.4004 35c-5.90039 -5.5 -25.2998 -21.3008 -30.4004 -35zM181.7 240.5c-2.5 0 -2.60059 -26.9004 1.7998 -40.7998c4.90039 8.7002 5.59961 40.7998 -1.7998 40.7998zM157.3 103.9
|
||||
c15.9004 6.09961 34 14.8994 54.7998 19.1992c-11.1992 8.30078 -21.7998 20.4004 -30.0996 35.5c-6.7002 -17.6992 -15 -37.7998 -24.7002 -54.6992zM288.9 108.9c3.59961 2.39941 -2.2002 10.3994 -37.3008 7.7998c32.3008 -13.7998 37.3008 -7.7998 37.3008 -7.7998z" />
|
||||
<glyph glyph-name="file-word" unicode="" horiz-adv-x="384"
|
||||
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
|
||||
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM268.1 192v0.200195h15.8008c7.7998 0 13.5 -7.2998 11.5996 -14.9004c-4.2998 -17 -13.7002 -54.0996 -34.5 -136c-1.2998 -5.39941 -6.09961 -9.09961 -11.5996 -9.09961h-24.7002
|
||||
c-5.5 0 -10.2998 3.7998 -11.6006 9.09961c-5.2998 20.9004 -17.7998 71 -17.8994 71.4004l-2.90039 17.2998c-0.5 -5.2998 -1.5 -11.0996 -3 -17.2998l-17.8994 -71.4004c-1.30078 -5.39941 -6.10059 -9.09961 -11.6006 -9.09961h-25.2002
|
||||
c-5.59961 0 -10.3994 3.7002 -11.6992 9.09961c-6.5 26.5 -25.2002 103.4 -33.2002 136c-1.7998 7.5 3.89941 14.7998 11.7002 14.7998h16.7998c5.7998 0 10.7002 -4.09961 11.7998 -9.69922c5 -25.7002 18.4004 -93.8008 19.0996 -99
|
||||
c0.300781 -1.7002 0.400391 -3.10059 0.5 -4.2002c0.800781 7.5 0.400391 4.7002 24.8008 103.7c1.39941 5.2998 6.19922 9.09961 11.6992 9.09961h13.3008c5.59961 0 10.3994 -3.7998 11.6992 -9.2002c23.9004 -99.7002 22.8008 -94.3994 23.6006 -99.5
|
||||
c0.299805 -1.7002 0.5 -3.09961 0.700195 -4.2998c0.599609 8.09961 0.399414 5.7998 21 103.5c1.09961 5.5 6 9.5 11.6992 9.5z" />
|
||||
<glyph glyph-name="file-excel" unicode="" horiz-adv-x="384"
|
||||
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
|
||||
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM260 224c9.2002 0 15 -10 10.2998 -18c-16 -27.5 -45.5996 -76.9004 -46.2998 -78l46.4004 -78c4.59961 -8 -1.10059 -18 -10.4004 -18h-28.7998c-4.40039 0 -8.5 2.40039 -10.6006 6.2998
|
||||
c-22.6992 41.7998 -13.6992 27.5 -28.5996 57.7002c-5.59961 -12.7002 -6.90039 -17.7002 -28.5996 -57.7002c-2.10059 -3.89941 -6.10059 -6.2998 -10.5 -6.2998h-28.9004c-9.2998 0 -15.0996 10 -10.4004 18l46.3008 78l-46.3008 78c-4.59961 8 1.10059 18 10.4004 18
|
||||
h28.9004c4.39941 0 8.5 -2.40039 10.5996 -6.2998c21.7002 -40.4004 14.7002 -28.6006 28.5996 -57.7002c6.40039 15.2998 10.6006 24.5996 28.6006 57.7002c2.09961 3.89941 6.09961 6.2998 10.5 6.2998h28.7998z" />
|
||||
<glyph glyph-name="file-powerpoint" unicode="" horiz-adv-x="384"
|
||||
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
|
||||
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM120 44v168c0 6.59961 5.40039 12 12 12h69.2002c36.7002 0 62.7998 -27 62.7998 -66.2998c0 -74.2998 -68.7002 -66.5 -95.5 -66.5v-47.2002c0 -6.59961 -5.40039 -12 -12 -12h-24.5c-6.59961 0 -12 5.40039 -12 12z
|
||||
M168.5 131.4h23c7.90039 0 13.9004 2.39941 18.0996 7.19922c8.5 9.80078 8.40039 28.5 0.100586 37.8008c-4.10059 4.59961 -9.90039 7 -17.4004 7h-23.8994v-52h0.0996094z" />
|
||||
<glyph glyph-name="file-image" unicode="" horiz-adv-x="384"
|
||||
d="M369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM332.1 320l-76.0996 76.0996v-76.0996h76.0996zM48 -16h288v288
|
||||
h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416zM80 32v64l39.5 39.5c4.7002 4.7002 12.2998 4.7002 17 0l39.5 -39.5l87.5 87.5c4.7002 4.7002 12.2998 4.7002 17 0l23.5 -23.5v-128h-224zM128 272c26.5 0 48 -21.5 48 -48s-21.5 -48 -48 -48s-48 21.5 -48 48
|
||||
s21.5 48 48 48z" />
|
||||
<glyph glyph-name="file-archive" unicode="" horiz-adv-x="384"
|
||||
d="M128.3 288h32v-32h-32v32zM192.3 384v-32h-32v32h32zM128.3 352h32v-32h-32v32zM192.3 320v-32h-32v32h32zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1
|
||||
c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-48.2998v-16h-32v16h-79.7002v-416h288zM194.2 182.3l17.2998 -87.7002c6.40039 -32.3994 -18.4004 -62.5996 -51.5 -62.5996
|
||||
c-33.2002 0 -58 30.4004 -51.4004 62.9004l19.7002 97.0996v32h32v-32h22.1006c5.7998 0 10.6992 -4.09961 11.7998 -9.7002zM160.3 57.9004c17.9004 0 32.4004 12.0996 32.4004 27c0 14.8994 -14.5 27 -32.4004 27c-17.8994 0 -32.3994 -12.1006 -32.3994 -27
|
||||
c0 -14.9004 14.5 -27 32.3994 -27zM192.3 256v-32h-32v32h32z" />
|
||||
<glyph glyph-name="file-audio" unicode="" horiz-adv-x="384"
|
||||
d="M369.941 350.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM332.118 320
|
||||
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM192 60.0244c0 -10.6914 -12.9258 -16.0459 -20.4854 -8.48535l-35.5146 35.9746h-28c-6.62695 0 -12 5.37305 -12 12v56c0 6.62695 5.37305 12 12 12h28
|
||||
l35.5146 36.9473c7.56055 7.56055 20.4854 2.20605 20.4854 -8.48535v-135.951zM233.201 107.154c9.05078 9.29688 9.05957 24.1328 0.000976562 33.4385c-22.1494 22.752 12.2344 56.2461 34.3945 33.4814c27.1982 -27.9404 27.2119 -72.4443 0.000976562 -100.401
|
||||
c-21.793 -22.3857 -56.9463 10.3154 -34.3965 33.4814z" />
|
||||
<glyph glyph-name="file-video" unicode="" horiz-adv-x="384"
|
||||
d="M369.941 350.059c7.75977 -7.75977 14.0586 -22.9658 14.0586 -33.9404v-332.118c0 -26.5098 -21.4902 -48 -48 -48h-288c-26.5098 0 -48 21.4902 -48 48v416c0 26.5098 21.4902 48 48 48h204.118c10.9746 0 26.1807 -6.29883 33.9404 -14.0586zM332.118 320
|
||||
l-76.1182 76.1182v-76.1182h76.1182zM48 -16h288v288h-104c-13.2549 0 -24 10.7451 -24 24v104h-160v-416zM276.687 195.303c10.0049 10.0049 27.3135 2.99707 27.3135 -11.3135v-111.976c0 -14.2939 -17.2959 -21.332 -27.3135 -11.3135l-52.6865 52.6738v-37.374
|
||||
c0 -11.0459 -8.9541 -20 -20 -20h-104c-11.0459 0 -20 8.9541 -20 20v104c0 11.0459 8.9541 20 20 20h104c11.0459 0 20 -8.9541 20 -20v-37.374z" />
|
||||
<glyph glyph-name="file-code" unicode="" horiz-adv-x="384"
|
||||
d="M149.9 98.9004c3.5 -3.30078 3.69922 -8.90039 0.399414 -12.4004l-17.3994 -18.5996c-1.60059 -1.80078 -4 -2.80078 -6.40039 -2.80078c-2.2002 0 -4.40039 0.900391 -6 2.40039l-57.7002 54.0996c-3.7002 3.40039 -3.7002 9.30078 0 12.8008l57.7002 54.0996
|
||||
c3.40039 3.2998 9 3.2002 12.4004 -0.400391l17.3994 -18.5996l0.200195 -0.200195c3.2002 -3.59961 2.7998 -9.2002 -0.799805 -12.3994l-32.7998 -28.9004l32.7998 -28.9004zM369.9 350.1c9 -9 14.0996 -21.2998 14.0996 -34v-332.1c0 -26.5 -21.5 -48 -48 -48h-288
|
||||
c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48.0996h204.1c12.7002 0 24.9004 -5.09961 33.9004 -14.0996zM256 396.1v-76.0996h76.0996zM336 -16v288h-104c-13.2998 0 -24 10.7002 -24 24v104h-160v-416h288zM209.6 234l24.4004 -7
|
||||
c4.7002 -1.2998 7.40039 -6.2002 6 -10.9004l-54.7002 -188.199c-1.2998 -4.60059 -6.2002 -7.40039 -10.8994 -6l-24.4004 7.09961c-4.7002 1.2998 -7.40039 6.2002 -6 10.9004l54.7002 188.1c1.39941 4.7002 6.2002 7.40039 10.8994 6zM234.1 157.1
|
||||
c-3.5 3.30078 -3.69922 8.90039 -0.399414 12.4004l17.3994 18.5996c3.30078 3.60059 8.90039 3.7002 12.4004 0.400391l57.7002 -54.0996c3.7002 -3.40039 3.7002 -9.30078 0 -12.8008l-57.7002 -54.0996c-3.5 -3.2998 -9.09961 -3.09961 -12.4004 0.400391
|
||||
l-17.3994 18.5996l-0.200195 0.200195c-3.2002 3.59961 -2.7998 9.2002 0.799805 12.3994l32.7998 28.9004l-32.7998 28.9004z" />
|
||||
<glyph glyph-name="life-ring" unicode=""
|
||||
d="M256 -56c-136.967 0 -248 111.033 -248 248s111.033 248 248 248s248 -111.033 248 -248s-111.033 -248 -248 -248zM152.602 20.7197c63.2178 -38.3184 143.579 -38.3184 206.797 0l-53.4111 53.4111c-31.8467 -13.5215 -68.168 -13.5059 -99.9746 0zM336 192
|
||||
c0 44.1123 -35.8877 80 -80 80s-80 -35.8877 -80 -80s35.8877 -80 80 -80s80 35.8877 80 80zM427.28 88.6016c38.3184 63.2178 38.3184 143.579 0 206.797l-53.4111 -53.4111c13.5215 -31.8467 13.5049 -68.168 0 -99.9746zM359.397 363.28
|
||||
c-63.2168 38.3184 -143.578 38.3184 -206.796 0l53.4111 -53.4111c31.8457 13.5215 68.167 13.5049 99.9736 0zM84.7197 295.398c-38.3184 -63.2178 -38.3184 -143.579 0 -206.797l53.4111 53.4111c-13.5215 31.8467 -13.5059 68.168 0 99.9746z" />
|
||||
<glyph glyph-name="paper-plane" unicode=""
|
||||
d="M440 441.5c34.5996 19.9004 77.5996 -8.7998 71.5 -48.9004l-59.4004 -387.199c-2.2998 -14.5 -11.0996 -27.3008 -23.8994 -34.5c-7.2998 -4.10059 -15.4004 -6.2002 -23.6006 -6.2002c-6.19922 0 -12.3994 1.2002 -18.2998 3.59961l-111.899 46.2002l-43.8008 -59.0996
|
||||
c-27.3994 -36.9004 -86.5996 -17.8008 -86.5996 28.5996v84.4004l-114.3 47.2998c-36.7998 15.0996 -40.1006 66 -5.7002 85.8994zM192 -16l36.5996 49.5l-36.5996 15.0996v-64.5996zM404.6 12.7002l59.4004 387.3l-416 -240l107.8 -44.5996l211.5 184.3
|
||||
c14.2002 12.2998 34.4004 -5.7002 23.7002 -21.2002l-140.2 -202.3z" />
|
||||
<glyph glyph-name="futbol" unicode="" horiz-adv-x="496"
|
||||
d="M483.8 268.6c42.2998 -130.199 -29 -270.1 -159.2 -312.399c-25.5 -8.2998 -51.2998 -12.2002 -76.6992 -12.2002c-104.5 0 -201.7 66.5996 -235.7 171.4c-42.2998 130.199 29 270.1 159.2 312.399c25.5 8.2998 51.2998 12.2002 76.6992 12.2002
|
||||
c104.5 0 201.7 -66.5996 235.7 -171.4zM409.3 74.9004c6.10059 8.39941 12.1006 16.8994 16.7998 26.1992c14.3008 28.1006 21.5 58.5 21.7002 89.2002l-38.8994 36.4004l-71.1006 -22.1006l-24.3994 -75.1992l43.6992 -60.9004zM409.3 310.3
|
||||
c-24.5 33.4004 -58.7002 58.4004 -97.8994 71.4004l-47.4004 -26.2002v-73.7998l64.2002 -46.5l70.7002 22zM184.9 381.6c-39.9004 -13.2998 -73.5 -38.5 -97.8008 -71.8994l10.1006 -52.5l70.5996 -22l64.2002 46.5v73.7998zM139 68.5l43.5 61.7002l-24.2998 74.2998
|
||||
l-71.1006 22.2002l-39 -36.4004c0.5 -55.7002 23.4004 -95.2002 37.8008 -115.3zM187.2 1.5c64.0996 -20.4004 115.5 -1.7998 121.7 0l22.3994 48.0996l-44.2998 61.7002h-78.5996l-43.6006 -61.7002z" />
|
||||
<glyph glyph-name="newspaper" unicode="" horiz-adv-x="576"
|
||||
d="M552 384c13.2549 0 24 -10.7451 24 -24v-336c0 -13.2549 -10.7451 -24 -24 -24h-496c-30.9277 0 -56 25.0723 -56 56v272c0 13.2549 10.7451 24 24 24h42.752c6.60547 18.623 24.3896 32 45.248 32h440zM48 56c0 -4.41113 3.58887 -8 8 -8s8 3.58887 8 8v248h-16v-248z
|
||||
M528 48v288h-416v-280c0 -2.7168 -0.204102 -5.38574 -0.578125 -8h416.578zM172 168c-6.62695 0 -12 5.37305 -12 12v96c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-96c0 -6.62695 -5.37305 -12 -12 -12h-136zM200 248v-40h80v40h-80zM160 108v24
|
||||
c0 6.62695 5.37305 12 12 12h136c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-136c-6.62695 0 -12 5.37305 -12 12zM352 108v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104
|
||||
c-6.62695 0 -12 5.37305 -12 12zM352 252v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12zM352 180v24c0 6.62695 5.37305 12 12 12h104c6.62695 0 12 -5.37305 12 -12v-24
|
||||
c0 -6.62695 -5.37305 -12 -12 -12h-104c-6.62695 0 -12 5.37305 -12 12z" />
|
||||
<glyph glyph-name="bell-slash" unicode="" horiz-adv-x="640"
|
||||
d="M633.99 -23.0195c6.91016 -5.52051 8.01953 -15.5908 2.5 -22.4902l-10 -12.4902c-5.53027 -6.88965 -15.5898 -8.00977 -22.4902 -2.49023l-598 467.51c-6.90039 5.52051 -8.01953 15.5908 -2.49023 22.4902l10 12.4902
|
||||
c5.52051 6.90039 15.5898 8.00977 22.4902 2.49023zM163.53 80h182.84l61.3994 -48h-279.659c-19.1201 0 -31.9902 15.5996 -32.1006 32c-0.0498047 7.5498 2.61035 15.2598 8.61035 21.71c18.3701 19.7402 51.5703 49.6904 54.8398 140.42l45.4697 -35.5498
|
||||
c-6.91992 -54.7803 -24.6895 -88.5498 -41.3994 -110.58zM320 352c-23.3496 0 -45 -7.17969 -62.9404 -19.4004l-38.1699 29.8408c19.6807 15.7793 43.1104 27.3096 69.1299 32.7197v20.8398c0 17.6699 14.3203 32 31.9805 32s31.9805 -14.3301 31.9805 -32v-20.8398
|
||||
c73.46 -15.2598 127.939 -77.46 127.939 -155.16c0 -41.3604 6.03027 -70.7197 14.3398 -92.8496l-59.5293 46.54c-1.63086 13.96 -2.77051 28.8896 -2.79004 45.7295c0 0.200195 0.0595703 0.379883 0.0595703 0.580078c0 61.8604 -50.1396 112 -112 112zM320 -64
|
||||
c-35.3203 0 -63.9697 28.6504 -63.9697 64h127.939c0 -35.3496 -28.6494 -64 -63.9697 -64z" />
|
||||
<glyph glyph-name="copyright" unicode=""
|
||||
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
|
||||
M363.351 93.0645c-9.61328 -9.71289 -45.5293 -41.3965 -104.064 -41.3965c-82.4297 0 -140.484 61.4248 -140.484 141.567c0 79.1514 60.2754 139.4 139.763 139.4c55.5303 0 88.7373 -26.6201 97.5928 -34.7783c2.13379 -1.96289 3.86523 -5.9082 3.86523 -8.80762
|
||||
c0 -1.95508 -0.864258 -4.87402 -1.92969 -6.51465l-18.1543 -28.1133c-3.8418 -5.9502 -11.9668 -7.28223 -17.499 -2.9209c-8.5957 6.77637 -31.8145 22.5381 -61.708 22.5381c-48.3037 0 -77.916 -35.3301 -77.916 -80.082c0 -41.5889 26.8877 -83.6924 78.2764 -83.6924
|
||||
c32.6572 0 56.8428 19.0391 65.7266 27.2256c5.26953 4.85645 13.5957 4.03906 17.8193 -1.73828l19.8652 -27.1699c1.28613 -1.74512 2.33008 -4.91992 2.33008 -7.08789c0 -2.72363 -1.56055 -6.5 -3.48242 -8.42969z" />
|
||||
<glyph glyph-name="closed-captioning" unicode=""
|
||||
d="M464 384c26.5 0 48 -21.5 48 -48v-288c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v288c0 26.5 21.5 48 48 48h416zM458 48c3.2998 0 6 2.7002 6 6v276c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-276c0 -3.2998 2.7002 -6 6 -6h404z
|
||||
M246.9 133.7c1.69922 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.6006 -56.7998 -172.801 -32.0996 -172.801 67.9004c0 97.2998 121.7 119.5 172.5 70.0996c2.10059 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.89941 -3.09961 -6.19922 -4 -9.09961 -1.7002
|
||||
c-40.7998 32 -94.5996 14.9004 -94.5996 -31.1992c0 -48 51 -70.5 92.1992 -32.6006c2.80078 2.5 7.10059 2.10059 9.2002 -0.899414zM437.3 133.7c1.7002 -2.40039 1.5 -5.60059 -0.5 -7.7002c-53.5996 -56.9004 -172.8 -32.0996 -172.8 67.9004
|
||||
c0 97.2998 121.7 119.5 172.5 70.0996c2.09961 -2 2.5 -3.2002 1 -5.7002l-17.5 -30.5c-1.90039 -3.09961 -6.2002 -4 -9.09961 -1.7002c-40.8008 32 -94.6006 14.9004 -94.6006 -31.1992c0 -48 51 -70.5 92.2002 -32.6006c2.7998 2.5 7.09961 2.10059 9.2002 -0.899414z
|
||||
" />
|
||||
<glyph glyph-name="object-group" unicode=""
|
||||
d="M500 320h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v256h-12
|
||||
c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM448 384v-32h32v32h-32zM32 384v-32h32v32h-32zM64 0v32
|
||||
h-32v-32h32zM480 0v32h-32v-32h32zM440 64v256h-12c-6.62695 0 -12 5.37305 -12 12v12h-320v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-256h12c6.62695 0 12 -5.37305 12 -12v-12h320v12c0 6.62695 5.37305 12 12 12h12zM404 256c6.62695 0 12 -5.37207 12 -12v-168
|
||||
c0 -6.62793 -5.37305 -12 -12 -12h-200c-6.62695 0 -12 5.37207 -12 12v52h-84c-6.62695 0 -12 5.37207 -12 12v168c0 6.62793 5.37305 12 12 12h200c6.62695 0 12 -5.37207 12 -12v-52h84zM136 280v-112h144v112h-144zM376 104v112h-56v-76
|
||||
c0 -6.62793 -5.37305 -12 -12 -12h-76v-24h144z" />
|
||||
<glyph glyph-name="object-ungroup" unicode="" horiz-adv-x="576"
|
||||
d="M564 224h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v24h-88v-12
|
||||
c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v72c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h72
|
||||
c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-12v-24h88v12c0 6.62695 5.37305 12 12 12h72c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12zM352 384v-32h32v32h-32zM352 128v-32h32v32h-32zM64 96v32h-32v-32h32zM64 352v32
|
||||
h-32v-32h32zM96 136h224v12c0 6.62695 5.37305 12 12 12h12v160h-12c-6.62695 0 -12 5.37305 -12 12v12h-224v-12c0 -6.62695 -5.37305 -12 -12 -12h-12v-160h12c6.62695 0 12 -5.37305 12 -12v-12zM224 0v32h-32v-32h32zM504 64v160h-12c-6.62695 0 -12 5.37305 -12 12v12
|
||||
h-88v-88h12c6.62695 0 12 -5.37305 12 -12v-72c0 -6.62695 -5.37305 -12 -12 -12h-72c-6.62695 0 -12 5.37305 -12 12v12h-88v-24h12c6.62695 0 12 -5.37305 12 -12v-12h224v12c0 6.62695 5.37305 12 12 12h12zM544 0v32h-32v-32h32zM544 256v32h-32v-32h32z" />
|
||||
<glyph glyph-name="sticky-note" unicode="" horiz-adv-x="448"
|
||||
d="M448 99.8936c0 -10.9746 -6.29883 -26.1797 -14.0586 -33.9404l-83.8828 -83.8818c-7.75977 -7.76074 -22.9658 -14.0596 -33.9404 -14.0596h-268.118c-26.5098 0 -48 21.4902 -48 48v351.988c0 26.5098 21.4902 48 48 48h352c26.5098 0 48 -21.4902 48 -48v-268.106z
|
||||
M320 19.8936l76.1182 76.1182h-76.1182v-76.1182zM400 368h-352v-351.988h224v104c0 13.2549 10.7451 24 24 24h104v223.988z" />
|
||||
<glyph glyph-name="clone" unicode=""
|
||||
d="M464 448c26.5098 0 48 -21.4902 48 -48v-320c0 -26.5098 -21.4902 -48 -48 -48h-48v-48c0 -26.5098 -21.4902 -48 -48 -48h-320c-26.5098 0 -48 21.4902 -48 48v320c0 26.5098 21.4902 48 48 48h48v48c0 26.5098 21.4902 48 48 48h320zM362 -16c3.31152 0 6 2.68848 6 6
|
||||
v42h-224c-26.5098 0 -48 21.4902 -48 48v224h-42c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308zM458 80c3.31152 0 6 2.68848 6 6v308c0 3.31152 -2.68848 6 -6 6h-308c-3.31152 0 -6 -2.68848 -6 -6v-308c0 -3.31152 2.68848 -6 6 -6h308z" />
|
||||
<glyph glyph-name="hourglass" unicode="" horiz-adv-x="384"
|
||||
d="M368 400c0 -80.0996 -31.8984 -165.619 -97.1797 -208c64.9912 -42.1934 97.1797 -127.436 97.1797 -208h4c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-360c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h4
|
||||
c0 80.0996 31.8994 165.619 97.1797 208c-64.9912 42.1934 -97.1797 127.436 -97.1797 208h-4c-6.62695 0 -12 5.37305 -12 12v24c0 6.62695 5.37305 12 12 12h360c6.62695 0 12 -5.37305 12 -12v-24c0 -6.62695 -5.37305 -12 -12 -12h-4zM64 400
|
||||
c0 -101.621 57.3066 -184 128 -184s128 82.3799 128 184h-256zM320 -16c0 101.62 -57.3076 184 -128 184s-128 -82.3799 -128 -184h256z" />
|
||||
<glyph glyph-name="hand-rock" unicode=""
|
||||
d="M408.864 368.948c48.8213 20.751 103.136 -15.0723 103.136 -67.9111v-114.443c0 -15.3955 -3.08887 -30.3906 -9.18262 -44.5674l-42.835 -99.6562c-4.99707 -11.625 -3.98242 -18.8574 -3.98242 -42.3701c0 -17.6729 -14.3271 -32 -32 -32h-252
|
||||
c-17.6729 0 -32 14.3271 -32 32c0 27.3301 1.1416 29.2012 -3.11035 32.9033l-97.71 85.0811c-24.8994 21.6797 -39.1797 52.8926 -39.1797 85.6338v56.9531c0 47.4277 44.8457 82.0215 91.0459 71.1807c1.96094 55.751 63.5107 87.8262 110.671 60.8057
|
||||
c29.1895 31.0713 78.8604 31.4473 108.334 -0.0214844c32.7051 18.6846 76.4121 10.3096 98.8135 -23.5879zM464 186.594v114.445c0 34.29 -52 33.8232 -52 0.676758c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v26.751
|
||||
c0 34.457 -52 33.707 -52 0.676758v-27.4287c0 -8.83594 -7.16309 -16 -16 -16h-7c-8.83691 0 -16 7.16406 -16 16v40.4658c0 34.3525 -52 33.8115 -52 0.677734v-41.1436c0 -8.83594 -7.16406 -16 -16 -16h-7c-8.83594 0 -16 7.16406 -16 16v26.751
|
||||
c0 34.4023 -52 33.7744 -52 0.676758v-116.571c0 -8.83203 -7.16797 -16 -16 -16c-3.30664 0 -8.01367 1.7627 -10.5068 3.93359l-7 6.09473c-3.03223 2.64062 -5.49316 8.04688 -5.49316 12.0674v0v41.2275c0 34.2148 -52 33.8857 -52 0.677734v-56.9531
|
||||
c0 -18.8555 8.27441 -36.874 22.7002 -49.4365l97.71 -85.0801c12.4502 -10.8398 19.5898 -26.4463 19.5898 -42.8164v-10.2861h220v7.07617c0 13.21 2.65332 26.0791 7.88281 38.25l42.835 99.6553c2.91602 6.75391 5.28223 18.207 5.28223 25.5635v0.0488281z" />
|
||||
<glyph glyph-name="hand-paper" unicode="" horiz-adv-x="448"
|
||||
d="M372.57 335.359c39.9062 5.63281 75.4297 -25.7393 75.4297 -66.3594v-131.564c-0.00195312 -12.7666 -2.33008 -33.2246 -5.19531 -45.666l-30.1836 -130.958c-3.34668 -14.5234 -16.2783 -24.8125 -31.1816 -24.8125h-222.897
|
||||
c-9.10352 0 -20.7793 6.01758 -26.0615 13.4316l-119.97 168.415c-21.2441 29.8203 -14.8047 71.3574 14.5498 93.1533c18.7754 13.9395 42.1309 16.2979 62.083 8.87109v126.13c0 44.0547 41.125 75.5439 82.4053 64.9834c23.8926 48.1963 92.3535 50.2471 117.982 0.74707
|
||||
c42.5186 11.1445 83.0391 -21.9346 83.0391 -65.5469v-10.8242zM399.997 137.437l-0.00195312 131.563c0 24.9492 -36.5703 25.5508 -36.5703 -0.691406v-76.3086c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v154.184
|
||||
c0 25.501 -36.5703 26.3633 -36.5703 0.691406v-154.875c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v188.309c0 25.501 -36.5703 26.3545 -36.5703 0.691406v-189c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16
|
||||
v153.309c0 25.501 -36.5713 26.3359 -36.5713 0.691406v-206.494c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832l-27.1279 38.0791c-14.3711 20.1709 -43.833 -2.33496 -29.3945 -22.6045l115.196 -161.697h201.92l27.3252 118.551
|
||||
c2.63086 11.417 3.96484 23.1553 3.96484 34.8857z" />
|
||||
<glyph glyph-name="hand-scissors" unicode=""
|
||||
d="M256 -32c-44.9561 0 -77.3428 43.2627 -64.0244 85.8535c-21.6484 13.71 -34.0156 38.7617 -30.3408 65.0068h-87.6348c-40.8037 0 -74 32.8105 -74 73.1406c0 40.3291 33.1963 73.1396 74 73.1396l94 -9.14062l-78.8496 18.6787
|
||||
c-38.3076 14.7422 -57.04 57.4707 -41.9424 95.1123c15.0303 37.4736 57.7549 55.7803 95.6416 41.2012l144.929 -55.7568c24.9551 30.5566 57.8086 43.9932 92.2178 24.7324l97.999 -54.8525c20.9746 -11.7393 34.0049 -33.8457 34.0049 -57.6904v-205.702
|
||||
c0 -30.7422 -21.4404 -57.5576 -51.7979 -64.5537l-118.999 -27.4268c-4.97168 -1.14648 -10.0889 -1.72949 -15.2031 -1.72949zM256 16.0127l70 -0.000976562c1.23633 0 3.21777 0.225586 4.42285 0.501953l119.001 27.4277
|
||||
c8.58203 1.97754 14.5762 9.29102 14.5762 17.7812v205.701c0 6.4873 -3.62109 12.542 -9.44922 15.8047l-98 54.8545c-8.13965 4.55566 -18.668 2.61914 -24.4873 -4.50781l-21.7646 -26.6475c-2.65039 -3.24512 -8.20215 -5.87891 -12.3926 -5.87891
|
||||
c-1.64062 0 -4.21484 0.477539 -5.74609 1.06738l-166.549 64.0908c-32.6543 12.5664 -50.7744 -34.5771 -19.2227 -46.7168l155.357 -59.7852c5.66016 -2.17773 10.2539 -8.86816 10.2539 -14.9326v0v-11.6328c0 -8.83691 -7.16309 -16 -16 -16h-182
|
||||
c-34.375 0 -34.4297 -50.2803 0 -50.2803h182c8.83691 0 16 -7.16309 16 -16v-6.85645c0 -8.83691 -7.16309 -16 -16 -16h-28c-25.1221 0 -25.1592 -36.5674 0 -36.5674h28c8.83691 0 16 -7.16211 16 -16v-6.85547c0 -8.83691 -7.16309 -16 -16 -16
|
||||
c-25.1201 0 -25.1602 -36.5674 0 -36.5674z" />
|
||||
<glyph glyph-name="hand-lizard" unicode="" horiz-adv-x="576"
|
||||
d="M556.686 157.458c12.6357 -19.4863 19.3145 -42.0615 19.3145 -65.2871v-124.171h-224v71.582l-99.751 38.7871c-2.7832 1.08203 -5.70996 1.63086 -8.69727 1.63086h-131.552c-30.8789 0 -56 25.1211 -56 56c0 48.5234 39.4766 88 88 88h113.709l18.333 48h-196.042
|
||||
c-44.1123 0 -80 35.8877 -80 80v8c0 30.8779 25.1211 56 56 56h293.917c24.5 0 47.084 -12.2725 60.4111 -32.8291zM528 16v76.1709v0.0478516c0 11.7461 -5.19141 29.2734 -11.5879 39.124l-146.358 225.715c-4.44336 6.85254 -11.9707 10.9424 -20.1367 10.9424h-293.917
|
||||
c-4.41113 0 -8 -3.58887 -8 -8v-8c0 -17.6445 14.3555 -32 32 -32h213.471c25.2021 0 42.626 -25.293 33.6299 -48.8457l-24.5518 -64.2812c-7.05371 -18.4658 -25.0732 -30.873 -44.8398 -30.873h-113.709c-22.0557 0 -40 -17.9443 -40 -40c0 -4.41113 3.58887 -8 8 -8
|
||||
h131.552h0.0517578c7.44141 0 19.1074 -2.19238 26.041 -4.89355l99.752 -38.7881c18.5898 -7.22852 30.6035 -24.7881 30.6035 -44.7363v-23.582h128z" />
|
||||
<glyph glyph-name="hand-spock" unicode=""
|
||||
d="M501.03 331.824c6.05762 -9.77832 10.9746 -27.0498 10.9746 -38.5518c0 -4.80664 -0.915039 -12.499 -2.04297 -17.1709l-57.623 -241.963c-12.748 -54.1729 -68.2627 -98.1387 -123.915 -98.1387h-0.345703h-107.455h-0.224609
|
||||
c-33.8135 0 -81.2148 18.834 -105.807 42.041l-91.3652 85.9766c-12.8213 12.0469 -23.2266 36.1016 -23.2266 53.6943c0 16.1299 8.97266 38.7529 20.0273 50.499c5.31836 5.66406 29.875 29.3926 68.1152 21.8477l-24.3594 82.1973
|
||||
c-1.68164 5.66406 -3.0459 15.0576 -3.0459 20.9668c0 37.5938 30.417 70.502 67.8955 73.4551c-0.204102 2.03125 -0.369141 5.33691 -0.369141 7.37891c0 31.627 24.8594 63.6895 55.4902 71.5684c43.248 10.9785 80.5645 -17.7012 89.6602 -53.0723l13.6836 -53.207
|
||||
l4.64648 22.6602c6.76074 32.417 39.123 58.8115 72.2373 58.916c8.73438 0 56.625 -3.26953 70.7383 -54.0801c15.0664 0.710938 46.9199 -3.50977 66.3105 -35.0176zM463.271 287.219c7.86914 32.9844 -42.1211 45.2695 -50.0859 11.9219l-24.8008 -104.146
|
||||
c-4.38867 -18.4141 -31.7783 -11.8926 -28.0557 6.2168l28.5479 139.166c7.39844 36.0703 -43.3076 45.0703 -50.1182 11.9629l-31.791 -154.971c-3.54883 -17.3086 -28.2832 -18.0469 -32.7109 -0.804688l-47.3262 184.035
|
||||
c-8.43359 32.8105 -58.3691 20.2676 -49.8652 -12.8359l42.4414 -165.039c4.81641 -18.7207 -23.3711 -26.9121 -28.9648 -8.00781l-31.3438 105.779c-9.6875 32.6465 -59.1191 18.2578 -49.3867 -14.625l36.0137 -121.539
|
||||
c5.61816 -18.9521 10.1777 -50.377 10.1777 -70.1436v-0.00878906c0 -6.54297 -8.05664 -10.9355 -13.4824 -5.82617l-51.123 48.1074c-24.7852 23.4082 -60.0527 -14.1875 -35.2793 -37.4902l91.3691 -85.9805c16.9629 -16.0068 49.6592 -28.998 72.9824 -28.998h0.154297
|
||||
h107.455h0.216797c34.7402 0 69.3936 27.4443 77.3525 61.2598z" />
|
||||
<glyph glyph-name="hand-pointer" unicode="" horiz-adv-x="448"
|
||||
d="M358.182 268.639c43.1934 16.6348 89.8184 -15.7949 89.8184 -62.6387v-84c-0.000976562 -4.25 -0.775391 -11.0615 -1.72754 -15.2041l-27.4297 -118.999c-6.98242 -30.2969 -33.7549 -51.7969 -64.5566 -51.7969h-178.286c-21.2588 0 -41.3682 10.4102 -53.791 27.8457
|
||||
l-109.699 154.001c-21.2432 29.8193 -14.8047 71.3574 14.5498 93.1523c18.8115 13.9658 42.1748 16.2822 62.083 8.87207v161.129c0 36.9443 29.7363 67 66.2861 67s66.2861 -30.0557 66.2861 -67v-73.6338c20.4131 2.85742 41.4678 -3.94238 56.5947 -19.6289
|
||||
c27.1934 12.8467 60.3799 5.66992 79.8721 -19.0986zM80.9854 168.303c-14.4004 20.2119 -43.8008 -2.38281 -29.3945 -22.6055l109.712 -154c3.43457 -4.81934 8.92871 -7.69727 14.6973 -7.69727h178.285c8.49219 0 15.8037 5.99414 17.7822 14.5762l27.4297 119.001
|
||||
c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285v84c0 25.1602 -36.5713 25.1211 -36.5713 0c0 -8.83594 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16406 -16 16v21c0 25.1602 -36.5713 25.1201 -36.5713 0v-21c0 -8.83594 -7.16309 -16 -16 -16h-6.85938
|
||||
c-8.83691 0 -16 7.16406 -16 16v35c0 25.1602 -36.5703 25.1201 -36.5703 0v-35c0 -8.83594 -7.16309 -16 -16 -16h-6.85742c-8.83691 0 -16 7.16406 -16 16v175c0 25.1602 -36.5713 25.1201 -36.5713 0v-241.493c0 -15.5703 -20.0352 -21.9092 -29.0303 -9.2832z
|
||||
M176.143 48v96c0 8.83691 6.26855 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16zM251.571 48v96c0 8.83691 6.26758 16 14 16h6c7.73145 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26855 -16 -14 -16h-6
|
||||
c-7.73242 0 -14 7.16309 -14 16zM327 48v96c0 8.83691 6.26758 16 14 16h6c7.73242 0 14 -7.16309 14 -16v-96c0 -8.83691 -6.26758 -16 -14 -16h-6c-7.73242 0 -14 7.16309 -14 16z" />
|
||||
<glyph glyph-name="hand-peace" unicode="" horiz-adv-x="448"
|
||||
d="M362.146 256.024c42.5908 13.3184 85.8535 -19.0684 85.8535 -64.0244l-0.0117188 -70.001c-0.000976562 -4.25 -0.775391 -11.0615 -1.72949 -15.2031l-27.4268 -118.999c-6.99707 -30.3564 -33.8105 -51.7969 -64.5547 -51.7969h-205.702
|
||||
c-23.8447 0 -45.9502 13.0303 -57.6904 34.0059l-54.8525 97.999c-19.2607 34.4092 -5.82422 67.2617 24.7324 92.2178l-55.7568 144.928c-14.5791 37.8867 3.72754 80.6113 41.2012 95.6416c37.6406 15.0977 80.3691 -3.63477 95.1123 -41.9424l18.6787 -78.8496
|
||||
l-9.14062 94c0 40.8037 32.8096 74 73.1396 74s73.1406 -33.1963 73.1406 -74v-87.6348c26.2451 3.6748 51.2959 -8.69238 65.0068 -30.3408zM399.987 122l-0.000976562 70c0 25.1602 -36.5674 25.1201 -36.5674 0c0 -8.83691 -7.16309 -16 -16 -16h-6.85547
|
||||
c-8.83789 0 -16 7.16309 -16 16v28c0 25.1592 -36.5674 25.1221 -36.5674 0v-28c0 -8.83691 -7.16309 -16 -16 -16h-6.85645c-8.83691 0 -16 7.16309 -16 16v182c0 34.4297 -50.2803 34.375 -50.2803 0v-182c0 -8.83691 -7.16309 -16 -16 -16h-11.6328v0
|
||||
c-6.06445 0 -12.7549 4.59375 -14.9326 10.2539l-59.7842 155.357c-12.1396 31.5518 -59.2842 13.4326 -46.7168 -19.2227l64.0898 -166.549c0.589844 -1.53125 1.06738 -4.10547 1.06738 -5.74609c0 -4.19043 -2.63379 -9.74219 -5.87891 -12.3926l-26.6475 -21.7646
|
||||
c-7.12695 -5.81934 -9.06445 -16.3467 -4.50781 -24.4873l54.8535 -98c3.26367 -5.82812 9.31934 -9.44922 15.8057 -9.44922h205.701c8.49121 0 15.8037 5.99414 17.7812 14.5762l27.4277 119.001c0.333008 1.44629 0.501953 2.93457 0.501953 4.42285z" />
|
||||
<glyph glyph-name="registered" unicode=""
|
||||
d="M256 440c136.967 0 248 -111.033 248 -248s-111.033 -248 -248 -248s-248 111.033 -248 248s111.033 248 248 248zM256 -8c110.549 0 200 89.4678 200 200c0 110.549 -89.4678 200 -200 200c-110.549 0 -200 -89.4688 -200 -200c0 -110.549 89.4678 -200 200 -200z
|
||||
M366.442 73.791c4.40332 -7.99219 -1.37012 -17.791 -10.5107 -17.791h-42.8096h-0.0126953c-3.97559 0 -8.71582 2.84961 -10.5801 6.36035l-47.5156 89.3027h-31.958v-83.6631c0 -6.61719 -5.38281 -12 -12 -12h-38.5674c-6.61719 0 -12 5.38281 -12 12v248.304
|
||||
c0 6.61719 5.38281 12 12 12h78.667c71.251 0 101.498 -32.749 101.498 -85.252c0 -31.6123 -15.2148 -59.2969 -39.4824 -73.1758c3.02148 -4.61719 0.225586 0.199219 53.2715 -96.085zM256.933 208.094c20.9131 0 32.4307 11.5186 32.4316 32.4316
|
||||
c0 19.5752 -6.5127 31.709 -38.9297 31.709h-27.377v-64.1406h33.875z" />
|
||||
<glyph glyph-name="calendar-plus" unicode="" horiz-adv-x="448"
|
||||
d="M336 156v-24c0 -6.59961 -5.40039 -12 -12 -12h-76v-76c0 -6.59961 -5.40039 -12 -12 -12h-24c-6.59961 0 -12 5.40039 -12 12v76h-76c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h76v76c0 6.59961 5.40039 12 12 12h24c6.59961 0 12 -5.40039 12 -12
|
||||
v-76h76c6.59961 0 12 -5.40039 12 -12zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40
|
||||
c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="calendar-minus" unicode="" horiz-adv-x="448"
|
||||
d="M124 120c-6.59961 0 -12 5.40039 -12 12v24c0 6.59961 5.40039 12 12 12h200c6.59961 0 12 -5.40039 12 -12v-24c0 -6.59961 -5.40039 -12 -12 -12h-200zM448 336v-352c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52
|
||||
c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="calendar-times" unicode="" horiz-adv-x="448"
|
||||
d="M311.7 73.2998l-17 -17c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-53.7002 53.7998l-53.7002 -53.6992c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-17 17c-4.7002 4.69922 -4.7002 12.2998 0 17l53.7002 53.6992l-53.7002 53.7002c-4.7002 4.7002 -4.7002 12.2998 0 17
|
||||
l17 17c4.7002 4.7002 12.2998 4.7002 17 0l53.7002 -53.7002l53.7002 53.7002c4.7002 4.7002 12.2998 4.7002 17 0l17 -17c4.7002 -4.7002 4.7002 -12.2998 0 -17l-53.7998 -53.7998l53.6992 -53.7002c4.80078 -4.7002 4.80078 -12.2998 0.100586 -17zM448 336v-352
|
||||
c0 -26.5 -21.5 -48 -48 -48h-352c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h48v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h128v52c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-52h48c26.5 0 48 -21.5 48 -48zM400 -10
|
||||
v298h-352v-298c0 -3.2998 2.7002 -6 6 -6h340c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="calendar-check" unicode="" horiz-adv-x="448"
|
||||
d="M400 384c26.5098 0 48 -21.4902 48 -48v-352c0 -26.5098 -21.4902 -48 -48 -48h-352c-26.5098 0 -48 21.4902 -48 48v352c0 26.5098 21.4902 48 48 48h48v52c0 6.62695 5.37305 12 12 12h40c6.62695 0 12 -5.37305 12 -12v-52h128v52c0 6.62695 5.37305 12 12 12h40
|
||||
c6.62695 0 12 -5.37305 12 -12v-52h48zM394 -16c3.31152 0 6 2.68848 6 6v298h-352v-298c0 -3.31152 2.68848 -6 6 -6h340zM341.151 184.65l-142.31 -141.169c-4.70508 -4.66699 -12.3027 -4.6377 -16.9707 0.0673828l-75.0908 75.6992
|
||||
c-4.66699 4.70508 -4.6377 12.3027 0.0673828 16.9707l22.7197 22.5361c4.70508 4.66699 12.3027 4.63672 16.9697 -0.0693359l44.1035 -44.4609l111.072 110.182c4.70508 4.66699 12.3027 4.63672 16.9707 -0.0683594l22.5361 -22.7178
|
||||
c4.66699 -4.70508 4.63672 -12.3027 -0.0683594 -16.9697z" />
|
||||
<glyph glyph-name="map" unicode="" horiz-adv-x="576"
|
||||
d="M560.02 416c8.4502 0 15.9805 -6.83008 15.9805 -16.0195v-346.32c0 -11.9609 -9.01367 -25.2705 -20.1201 -29.71l-151.83 -52.8105c-5.32617 -1.7334 -14.1953 -3.13965 -19.7969 -3.13965c-5.7373 0 -14.8105 1.47363 -20.2529 3.29004l-172 60.71l-170.05 -62.8398
|
||||
c-1.99023 -0.790039 -4 -1.16016 -5.95996 -1.16016c-8.45996 0 -15.9902 6.83008 -15.9902 16.0195v346.32c0.00292969 11.959 9.0166 25.2686 20.1201 29.71l151.83 52.8105c6.43945 2.08984 13.1201 3.13965 19.8096 3.13965
|
||||
c5.73242 -0.00195312 14.8008 -1.47168 20.2402 -3.28027l172 -60.7197h0.00976562l170.05 62.8398c1.98047 0.790039 4 1.16016 5.95996 1.16016zM224 357.58v-285.97l128 -45.1904v285.97zM48 29.9502l127.36 47.0801l0.639648 0.229492v286.2l-128 -44.5303v-288.979z
|
||||
M528 65.0801v288.97l-127.36 -47.0693l-0.639648 -0.240234v-286.19z" />
|
||||
<glyph glyph-name="comment-alt" unicode=""
|
||||
d="M448 448c35.2998 0 64 -28.7002 64 -64v-288c0 -35.2998 -28.7002 -64 -64 -64h-144l-124.9 -93.5996c-2.19922 -1.7002 -4.69922 -2.40039 -7.09961 -2.40039c-6.2002 0 -12 4.90039 -12 12v84h-96c-35.2998 0 -64 28.7002 -64 64v288c0 35.2998 28.7002 64 64 64h384z
|
||||
M464 96v288c0 8.7998 -7.2002 16 -16 16h-384c-8.7998 0 -16 -7.2002 -16 -16v-288c0 -8.7998 7.2002 -16 16 -16h144v-60l67.2002 50.4004l12.7998 9.59961h160c8.7998 0 16 7.2002 16 16z" />
|
||||
<glyph glyph-name="pause-circle" unicode=""
|
||||
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48
|
||||
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16zM240 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-48c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h48c8.7998 0 16 -7.2002 16 -16z" />
|
||||
<glyph glyph-name="stop-circle" unicode=""
|
||||
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM352 272v-160c0 -8.7998 -7.2002 -16 -16 -16h-160
|
||||
c-8.7998 0 -16 7.2002 -16 16v160c0 8.7998 7.2002 16 16 16h160c8.7998 0 16 -7.2002 16 -16z" />
|
||||
<glyph glyph-name="handshake" unicode="" horiz-adv-x="640"
|
||||
d="M519.2 320.1h120.8v-255.699h-64c-17.5 0 -31.7998 14.1992 -31.9004 31.6992h-57.8994c-1.7998 -8.19922 -5.2998 -16.0996 -10.9004 -23l-26.2002 -32.2998c-15.7998 -19.3994 -41.8994 -25.5 -64 -16.7998c-13.5 -16.5996 -30.5996 -24 -48.7998 -24
|
||||
c-15.0996 0 -28.5996 5.09961 -41.0996 15.9004c-31.7998 -21.9004 -74.7002 -21.3008 -105.601 3.7998l-84.5996 76.3994h-9.09961c-0.100586 -17.5 -14.3008 -31.6992 -31.9004 -31.6992h-64v255.699h118l47.5996 47.6006c10.5 10.3994 24.8008 16.2998 39.6006 16.2998
|
||||
h226.8v0c12.7812 0 30.5225 -7.30273 39.5996 -16.2998zM48 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16zM438 103.3c2.7002 3.40039 2.2002 8.5 -1.2002 11.2998l-108.2 87.8008l-8.19922 -7.5
|
||||
c-40.3008 -36.8008 -86.7002 -11.8008 -101.5 4.39941c-26.7002 29 -25 74.4004 4.39941 101.3l38.7002 35.5h-56.7002c-2 -0.799805 -3.7002 -1.5 -5.7002 -2.2998l-61.6992 -61.5996h-41.9004v-128.101h27.7002l97.2998 -88
|
||||
c16.0996 -13.0996 41.4004 -10.5 55.2998 6.60059l15.6006 19.2002l36.7998 -31.5c3 -2.40039 12 -4.90039 18 2.39941l30 36.5l23.8994 -19.3994c3.5 -2.80078 8.5 -2.2002 11.3008 1.19922zM544 144.1v128h-44.7002l-61.7002 61.6006
|
||||
c-1.39941 1.5 -3.39941 2.2998 -5.5 2.2998l-83.6992 -0.200195c-10 0 -19.6006 -3.7002 -27 -10.5l-65.6006 -60.0996c-9.7002 -8.7998 -10.5 -24 -1.2002 -33.9004c8.90039 -9.39941 25.1006 -8.7002 34.6006 0l55.2002 50.6006c6.5 5.89941 16.5996 5.5 22.5996 -1
|
||||
l10.9004 -11.7002c6 -6.5 5.5 -16.6006 -1 -22.6006l-12.5 -11.3994l102.699 -83.4004c2.80078 -2.2998 5.40039 -4.89941 7.7002 -7.7002h69.2002zM592 96.4004c8.7998 0 16 7.09961 16 16c0 8.7998 -7.2002 16 -16 16s-16 -7.2002 -16 -16c0 -8.80078 7.2002 -16 16 -16z
|
||||
" />
|
||||
<glyph glyph-name="envelope-open" unicode=""
|
||||
d="M494.586 283.484c9.6123 -7.94824 17.4141 -24.5205 17.4141 -36.9932v-262.491c0 -26.5098 -21.4902 -48 -48 -48h-416c-26.5098 0 -48 21.4902 -48 48v262.515c0 12.5166 7.84668 29.1279 17.5146 37.0771c4.08008 3.35449 110.688 89.0996 135.15 108.549
|
||||
c22.6992 18.1426 60.1299 55.8594 103.335 55.8594c43.4365 0 81.2314 -38.1914 103.335 -55.8594c23.5283 -18.707 130.554 -104.773 135.251 -108.656zM464 -10v253.632v0.00488281c0 1.5791 -0.996094 3.66602 -2.22363 4.6582
|
||||
c-15.8633 12.8232 -108.793 87.5752 -132.366 106.316c-17.5527 14.0195 -49.7168 45.3887 -73.4102 45.3887c-23.6016 0 -55.2451 -30.8799 -73.4102 -45.3887c-23.5713 -18.7393 -116.494 -93.4795 -132.364 -106.293
|
||||
c-1.40918 -1.13965 -2.22559 -2.85254 -2.22559 -4.66504v-253.653c0 -3.31152 2.68848 -6 6 -6h404c3.31152 0 6 2.68848 6 6zM432.009 177.704c4.24902 -5.15918 3.46484 -12.7949 -1.74512 -16.9814c-28.9746 -23.2822 -59.2734 -47.5967 -70.9287 -56.8623
|
||||
c-22.6992 -18.1436 -60.1299 -55.8604 -103.335 -55.8604c-43.4521 0 -81.2871 38.2373 -103.335 55.8604c-11.2793 8.9668 -41.7441 33.4131 -70.9268 56.8643c-5.20996 4.1875 -5.99316 11.8223 -1.74512 16.9814l15.2578 18.5283
|
||||
c4.17773 5.07227 11.6572 5.84277 16.7793 1.72559c28.6182 -23.001 58.5654 -47.0352 70.5596 -56.5713c17.5527 -14.0195 49.7168 -45.3887 73.4102 -45.3887c23.6016 0 55.2461 30.8799 73.4102 45.3887c11.9941 9.53516 41.9434 33.5703 70.5625 56.5684
|
||||
c5.12207 4.11621 12.6016 3.3457 16.7783 -1.72656z" />
|
||||
<glyph glyph-name="address-book" unicode="" horiz-adv-x="448"
|
||||
d="M436 288h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-64h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12h-20v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48
|
||||
h320c26.5 0 48 -21.5 48 -48v-48h20c6.59961 0 12 -5.40039 12 -12v-40c0 -6.59961 -5.40039 -12 -12 -12zM368 -16v416h-320v-416h320zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM118.4 64
|
||||
c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002
|
||||
h-179.199z" />
|
||||
<glyph glyph-name="address-card" unicode="" horiz-adv-x="576"
|
||||
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v352h-480v-352h480zM208 192c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z
|
||||
M118.4 64c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002
|
||||
c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199zM360 128c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 192c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
|
||||
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 256c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112z" />
|
||||
<glyph glyph-name="user-circle" unicode="" horiz-adv-x="496"
|
||||
d="M248 344c53 0 96 -43 96 -96s-43 -96 -96 -96s-96 43 -96 96s43 96 96 96zM248 200c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
|
||||
c49.7002 0 95.0996 18.2998 130.1 48.4004c-14.8994 23 -40.3994 38.5 -69.5996 39.5c-20.7998 -6.5 -40.5996 -9.60059 -60.5 -9.60059s-39.7002 3.2002 -60.5 9.60059c-29.2002 -0.900391 -54.7002 -16.5 -69.5996 -39.5c35 -30.1006 80.3994 -48.4004 130.1 -48.4004z
|
||||
M410.7 76.0996c23.3994 32.7002 37.2998 72.7002 37.2998 115.9c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -43.2002 13.9004 -83.2002 37.2998 -115.9c24.5 31.4004 62.2002 51.9004 105.101 51.9004c10.1992 0 26.0996 -9.59961 57.5996 -9.59961
|
||||
c31.5996 0 47.4004 9.59961 57.5996 9.59961c43 0 80.7002 -20.5 105.101 -51.9004z" />
|
||||
<glyph glyph-name="id-badge" unicode="" horiz-adv-x="384"
|
||||
d="M336 448c26.5 0 48 -21.5 48 -48v-416c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v416c0 26.5 21.5 48 48 48h288zM336 -16v416h-288v-416h288zM144 336c-8.7998 0 -16 7.2002 -16 16s7.2002 16 16 16h96c8.7998 0 16 -7.2002 16 -16s-7.2002 -16 -16 -16
|
||||
h-96zM192 160c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64zM102.4 32c-12.4004 0 -22.4004 8.59961 -22.4004 19.2002v19.2002c0 31.7998 30.0996 57.5996 67.2002 57.5996c11.3994 0 17.8994 -8 44.7998 -8
|
||||
c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996v-19.2002c0 -10.6006 -10 -19.2002 -22.4004 -19.2002h-179.199z" />
|
||||
<glyph glyph-name="id-card" unicode="" horiz-adv-x="576"
|
||||
d="M528 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-480c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h480zM528 16v288h-480v-288h32.7998c-1 4.5 -0.799805 -3.59961 -0.799805 22.4004c0 31.7998 30.0996 57.5996 67.2002 57.5996
|
||||
c11.3994 0 17.8994 -8 44.7998 -8c26.0996 0 34 8 44.7998 8c37.1006 0 67.2002 -25.7998 67.2002 -57.5996c0 -26 0.0996094 -17.9004 -0.799805 -22.4004h224.8zM360 96c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16
|
||||
c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 160c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM360 224c-4.40039 0 -8 3.59961 -8 8v16c0 4.40039 3.59961 8 8 8h112
|
||||
c4.40039 0 8 -3.59961 8 -8v-16c0 -4.40039 -3.59961 -8 -8 -8h-112zM192 128c-35.2998 0 -64 28.7002 -64 64s28.7002 64 64 64s64 -28.7002 64 -64s-28.7002 -64 -64 -64z" />
|
||||
<glyph glyph-name="window-maximize" unicode=""
|
||||
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v234h-416v-234c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="window-minimize" unicode=""
|
||||
d="M480 -32h-448c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32h448c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
|
||||
<glyph glyph-name="window-restore" unicode=""
|
||||
d="M464 448c26.5 0 48 -21.5 48 -48v-320c0 -26.5 -21.5 -48 -48 -48h-48v-48c0 -26.5 -21.5 -48 -48 -48h-320c-26.5 0 -48 21.5 -48 48v320c0 26.5 21.5 48 48 48h48v48c0 26.5 21.5 48 48 48h320zM368 -16v208h-320v-208h320zM464 80v320h-320v-48h224
|
||||
c26.5 0 48 -21.5 48 -48v-224h48z" />
|
||||
<glyph glyph-name="snowflake" unicode="" horiz-adv-x="448"
|
||||
d="M440.1 92.7998c7.60059 -4.39941 10.1006 -14.2002 5.5 -21.7002l-7.89941 -13.8994c-4.40039 -7.7002 -14 -10.2998 -21.5 -5.90039l-39.2002 23l9.09961 -34.7002c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.5996l-15.2002 -4.09961
|
||||
c-8.39941 -2.30078 -17.0996 2.7998 -19.2998 11.2998l-21.2998 81l-71.9004 42.2002v-84.5l58.2998 -59.3008c6.10059 -6.19922 6.10059 -16.3994 0 -22.5996l-11.0996 -11.2998c-6.09961 -6.2002 -16.0996 -6.2002 -22.2002 0l-24.8994 25.3994v-46.0996
|
||||
c0 -8.7998 -7 -16 -15.7002 -16h-15.7002c-8.7002 0 -15.7002 7.2002 -15.7002 16v45.9004l-24.8994 -25.4004c-6.10059 -6.2002 -16.1006 -6.2002 -22.2002 0l-11.1006 11.2998c-6.09961 6.2002 -6.09961 16.4004 0 22.6006l58.3008 59.2998v84.5l-71.9004 -42.2002
|
||||
l-21.2998 -81c-2.2998 -8.5 -10.9004 -13.5996 -19.2998 -11.2998l-15.2002 4.09961c-8.40039 2.2998 -13.2998 11.1006 -11.1006 19.6006l9.10059 34.6992l-39.2002 -23c-7.5 -4.39941 -17.2002 -1.7998 -21.5 5.90039l-7.90039 13.9004
|
||||
c-4.2998 7.69922 -1.69922 17.5 5.80078 21.8994l39.1992 23l-34.0996 9.2998c-8.40039 2.30078 -13.2998 11.1006 -11.0996 19.6006l4.09961 15.5c2.2998 8.5 10.9004 13.5996 19.2998 11.2998l79.7002 -21.7002l71.9004 42.2002l-71.9004 42.2002l-79.7002 -21.7002
|
||||
c-8.39941 -2.2998 -17.0996 2.7998 -19.2998 11.2998l-4.09961 15.5c-2.30078 8.5 2.69922 17.2998 11.0996 19.6006l34.0996 9.09961l-39.1992 23c-7.60059 4.5 -10.1006 14.2002 -5.80078 21.9004l7.90039 13.8994c4.40039 7.7002 14 10.2998 21.5 5.90039l39.2002 -23
|
||||
l-9.10059 34.7002c-2.2998 8.5 2.7002 17.2998 11.1006 19.5996l15.2002 4.09961c8.39941 2.30078 17.0996 -2.7998 19.2998 -11.2998l21.2998 -81l71.9004 -42.2002v84.5l-58.3008 59.3008c-6.09961 6.19922 -6.09961 16.3994 0 22.5996l11.5 11.2998
|
||||
c6.10059 6.2002 16.1006 6.2002 22.2002 0l24.9004 -25.3994v46.0996c0 8.7998 7 16 15.7002 16h15.6992c8.7002 0 15.7002 -7.2002 15.7002 -16v-45.9004l24.9004 25.4004c6.09961 6.2002 16.0996 6.2002 22.2002 0l11.0996 -11.2998
|
||||
c6.09961 -6.2002 6.09961 -16.4004 0 -22.6006l-58.2998 -59.2998v-84.5l71.8994 42.2002l21.3008 81c2.2998 8.5 10.8994 13.5996 19.2998 11.2998l15.2002 -4.09961c8.39941 -2.2998 13.2998 -11.1006 11.0996 -19.6006l-9.09961 -34.6992l39.1992 23
|
||||
c7.5 4.39941 17.2002 1.7998 21.5 -5.90039l7.90039 -13.9004c4.2998 -7.69922 1.7002 -17.5 -5.7998 -21.8994l-39.2002 -23l34.0996 -9.2998c8.40039 -2.30078 13.3008 -11.1006 11.1006 -19.6006l-4.10059 -15.5c-2.2998 -8.5 -10.8994 -13.5996 -19.2998 -11.2998
|
||||
l-79.7002 21.7002l-71.8994 -42.2002l71.7998 -42.2002l79.7002 21.7002c8.39941 2.2998 17.0996 -2.7998 19.2998 -11.2998l4.09961 -15.5c2.30078 -8.5 -2.69922 -17.2998 -11.0996 -19.6006l-34.0996 -9.2998z" />
|
||||
<glyph glyph-name="trash-alt" unicode="" horiz-adv-x="448"
|
||||
d="M268 32c-6.62402 0 -12 5.37598 -12 12v216c0 6.62402 5.37598 12 12 12h24c6.62402 0 12 -5.37598 12 -12v-216c0 -6.62402 -5.37598 -12 -12 -12h-24zM432 368c8.83203 0 16 -7.16797 16 -16v-16c0 -8.83203 -7.16797 -16 -16 -16h-16v-336
|
||||
c0 -26.4961 -21.5039 -48 -48 -48h-288c-26.4961 0 -48 21.5039 -48 48v336h-16c-8.83203 0 -16 7.16797 -16 16v16c0 8.83203 7.16797 16 16 16h82.4102l34.0195 56.7002c7.71875 12.8613 26.1572 23.2998 41.1572 23.2998h0.00292969h100.82h0.0224609
|
||||
c15 0 33.4385 -10.4385 41.1572 -23.2998l34 -56.7002h82.4102zM171.84 397.09l-17.4502 -29.0898h139.221l-17.46 29.0898c-0.96582 1.60645 -3.26953 2.91016 -5.14355 2.91016h-0.00683594h-94h-0.0166016c-1.87402 0 -4.17871 -1.30371 -5.14355 -2.91016zM368 -16v336
|
||||
h-288v-336h288zM156 32c-6.62402 0 -12 5.37598 -12 12v216c0 6.62402 5.37598 12 12 12h24c6.62402 0 12 -5.37598 12 -12v-216c0 -6.62402 -5.37598 -12 -12 -12h-24z" />
|
||||
<glyph glyph-name="images" unicode="" horiz-adv-x="576"
|
||||
d="M480 32v-16c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256c0 26.5098 21.4902 48 48 48h16v-48h-10c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v10h48zM522 368h-372
|
||||
c-3.31152 0 -6 -2.68848 -6 -6v-244c0 -3.31152 2.68848 -6 6 -6h372c3.31152 0 6 2.68848 6 6v244c0 3.31152 -2.68848 6 -6 6zM528 416c26.5098 0 48 -21.4902 48 -48v-256c0 -26.5098 -21.4902 -48 -48 -48h-384c-26.5098 0 -48 21.4902 -48 48v256
|
||||
c0 26.5098 21.4902 48 48 48h384zM264 304c0 -22.0908 -17.9092 -40 -40 -40s-40 17.9092 -40 40s17.9092 40 40 40s40 -17.9092 40 -40zM192 208l39.5146 39.5146c4.68652 4.68652 12.2842 4.68652 16.9717 0l39.5137 -39.5146l103.515 103.515
|
||||
c4.68652 4.68652 12.2842 4.68652 16.9717 0l71.5137 -71.5146v-80h-288v48z" />
|
||||
<glyph glyph-name="clipboard" unicode="" horiz-adv-x="384"
|
||||
d="M336 384c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-288c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h80c0 35.2998 28.7002 64 64 64s64 -28.7002 64 -64h80zM192 408c-13.2998 0 -24 -10.7002 -24 -24s10.7002 -24 24 -24s24 10.7002 24 24
|
||||
s-10.7002 24 -24 24zM336 -10v340c0 3.2998 -2.7002 6 -6 6h-42v-36c0 -6.59961 -5.40039 -12 -12 -12h-168c-6.59961 0 -12 5.40039 -12 12v36h-42c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h276c3.2998 0 6 2.7002 6 6z" />
|
||||
<glyph glyph-name="arrow-alt-circle-down" unicode=""
|
||||
d="M256 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM256 -8c110.5 0 200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200zM224 308c0 6.59961 5.40039 12 12 12h40c6.59961 0 12 -5.40039 12 -12v-116
|
||||
h67c10.7002 0 16.0996 -12.9004 8.5 -20.5l-99 -99c-4.7002 -4.7002 -12.2998 -4.7002 -17 0l-99 99c-7.5 7.59961 -2.2002 20.5 8.5 20.5h67v116z" />
|
||||
<glyph glyph-name="arrow-alt-circle-left" unicode=""
|
||||
d="M8 192c0 137 111 248 248 248s248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248zM456 192c0 110.5 -89.5 200 -200 200s-200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200zM384 212v-40c0 -6.59961 -5.40039 -12 -12 -12h-116v-67
|
||||
c0 -10.7002 -12.9004 -16 -20.5 -8.5l-99 99c-4.7002 4.7002 -4.7002 12.2998 0 17l99 99c7.59961 7.59961 20.5 2.2002 20.5 -8.5v-67h116c6.59961 0 12 -5.40039 12 -12z" />
|
||||
<glyph glyph-name="arrow-alt-circle-right" unicode=""
|
||||
d="M504 192c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248s248 -111 248 -248zM56 192c0 -110.5 89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200s-200 -89.5 -200 -200zM128 172v40c0 6.59961 5.40039 12 12 12h116v67
|
||||
c0 10.7002 12.9004 16 20.5 8.5l99 -99c4.7002 -4.7002 4.7002 -12.2998 0 -17l-99 -99c-7.59961 -7.59961 -20.5 -2.2002 -20.5 8.5v67h-116c-6.59961 0 -12 5.40039 -12 12z" />
|
||||
<glyph glyph-name="arrow-alt-circle-up" unicode=""
|
||||
d="M256 -56c-137 0 -248 111 -248 248s111 248 248 248s248 -111 248 -248s-111 -248 -248 -248zM256 392c-110.5 0 -200 -89.5 -200 -200s89.5 -200 200 -200s200 89.5 200 200s-89.5 200 -200 200zM276 64h-40c-6.59961 0 -12 5.40039 -12 12v116h-67
|
||||
c-10.7002 0 -16 12.9004 -8.5 20.5l99 99c4.7002 4.7002 12.2998 4.7002 17 0l99 -99c7.59961 -7.59961 2.2002 -20.5 -8.5 -20.5h-67v-116c0 -6.59961 -5.40039 -12 -12 -12z" />
|
||||
<glyph glyph-name="gem" unicode="" horiz-adv-x="576"
|
||||
d="M464 448c4.09961 0 7.7998 -2 10.0996 -5.40039l99.9004 -147.199c2.90039 -4.40039 2.59961 -10.1006 -0.700195 -14.2002l-276 -340.8c-4.7998 -5.90039 -13.7998 -5.90039 -18.5996 0l-276 340.8c-3.2998 4 -3.60059 9.7998 -0.700195 14.2002l100 147.199
|
||||
c2.2002 3.40039 6 5.40039 10 5.40039h352zM444.7 400h-56.7998l51.6992 -96h68.4004zM242.6 400l-51.5996 -96h194l-51.7002 96h-90.7002zM131.3 400l-63.2998 -96h68.4004l51.6992 96h-56.7998zM88.2998 256l119.7 -160l-68.2998 160h-51.4004zM191.2 256l96.7998 -243.3
|
||||
l96.7998 243.3h-193.6zM368 96l119.6 160h-51.3994z" />
|
||||
<glyph glyph-name="money-bill-alt" unicode="" horiz-adv-x="640"
|
||||
d="M320 304c53.0195 0 96 -50.1396 96 -112c0 -61.8701 -43 -112 -96 -112c-53.0195 0 -96 50.1504 -96 112c0 61.8604 42.9805 112 96 112zM360 136v16c0 4.41992 -3.58008 8 -8 8h-16v88c0 4.41992 -3.58008 8 -8 8h-13.5801h-0.000976562
|
||||
c-4.01074 0 -9.97266 -1.80566 -13.3086 -4.03027l-15.3301 -10.2197c-1.96777 -1.30957 -3.56445 -4.29004 -3.56445 -6.65332c0 -1.33691 0.601562 -3.32422 1.34375 -4.43652l8.88086 -13.3105c1.30859 -1.9668 4.29004 -3.56445 6.65332 -3.56445
|
||||
c1.33691 0 3.32422 0.602539 4.43652 1.34473l0.469727 0.310547v-55.4404h-16c-4.41992 0 -8 -3.58008 -8 -8v-16c0 -4.41992 3.58008 -8 8 -8h64c4.41992 0 8 3.58008 8 8zM608 384c17.6699 0 32 -14.3301 32 -32v-320c0 -17.6699 -14.3301 -32 -32 -32h-576
|
||||
c-17.6699 0 -32 14.3301 -32 32v320c0 17.6699 14.3301 32 32 32h576zM592 112v160c-35.3496 0 -64 28.6504 -64 64h-416c0 -35.3496 -28.6504 -64 -64 -64v-160c35.3496 0 64 -28.6504 64 -64h416c0 35.3496 28.6504 64 64 64z" />
|
||||
<glyph glyph-name="window-close" unicode=""
|
||||
d="M464 416c26.5 0 48 -21.5 48 -48v-352c0 -26.5 -21.5 -48 -48 -48h-416c-26.5 0 -48 21.5 -48 48v352c0 26.5 21.5 48 48 48h416zM464 22v340c0 3.2998 -2.7002 6 -6 6h-404c-3.2998 0 -6 -2.7002 -6 -6v-340c0 -3.2998 2.7002 -6 6 -6h404c3.2998 0 6 2.7002 6 6z
|
||||
M356.5 253.4l-61.4004 -61.4004l61.4004 -61.4004c4.59961 -4.59961 4.59961 -12.0996 0 -16.7998l-22.2998 -22.2998c-4.60059 -4.59961 -12.1006 -4.59961 -16.7998 0l-61.4004 61.4004l-61.4004 -61.4004c-4.59961 -4.59961 -12.0996 -4.59961 -16.7998 0
|
||||
l-22.2998 22.2998c-4.59961 4.60059 -4.59961 12.1006 0 16.7998l61.4004 61.4004l-61.4004 61.4004c-4.59961 4.59961 -4.59961 12.0996 0 16.7998l22.2998 22.2998c4.60059 4.59961 12.1006 4.59961 16.7998 0l61.4004 -61.4004l61.4004 61.4004
|
||||
c4.59961 4.59961 12.0996 4.59961 16.7998 0l22.2998 -22.2998c4.7002 -4.60059 4.7002 -12.1006 0 -16.7998z" />
|
||||
<glyph glyph-name="comment-dots" unicode=""
|
||||
d="M144 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM256 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM368 240c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
|
||||
s-32 14.2998 -32 32s14.2998 32 32 32zM256 416c141.4 0 256 -93.0996 256 -208s-114.6 -208 -256 -208c-32.7998 0 -64 5.2002 -92.9004 14.2998c-29.0996 -20.5996 -77.5996 -46.2998 -139.1 -46.2998c-9.59961 0 -18.2998 5.7002 -22.0996 14.5
|
||||
c-3.80078 8.7998 -2 19 4.59961 26c0.5 0.400391 31.5 33.7998 46.4004 73.2002c-33 35.0996 -52.9004 78.7002 -52.9004 126.3c0 114.9 114.6 208 256 208zM256 48c114.7 0 208 71.7998 208 160s-93.2998 160 -208 160s-208 -71.7998 -208 -160
|
||||
c0 -42.2002 21.7002 -74.0996 39.7998 -93.4004l20.6006 -21.7998l-10.6006 -28.0996c-5.5 -14.5 -12.5996 -28.1006 -19.8994 -40.2002c23.5996 7.59961 43.1992 18.9004 57.5 29l19.5 13.7998l22.6992 -7.2002c25.3008 -8 51.7002 -12.0996 78.4004 -12.0996z" />
|
||||
<glyph glyph-name="smile-wink" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM365.8 138.4c10.2002 -8.5 11.6006 -23.6006 3.10059 -33.8008
|
||||
c-30 -36 -74.1006 -56.5996 -120.9 -56.5996s-90.9004 20.5996 -120.9 56.5996c-8.39941 10.2002 -7.09961 25.3008 3.10059 33.8008c10.0996 8.39941 25.2998 7.09961 33.7998 -3.10059c20.7998 -25.0996 51.5 -39.3994 84 -39.3994s63.2002 14.3994 84 39.3994
|
||||
c8.5 10.2002 23.5996 11.6006 33.7998 3.10059zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 268c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008l-9.5 8.5
|
||||
c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996z" />
|
||||
<glyph glyph-name="angry" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 136c33.5996 0 65.2002 -14.7998 86.7998 -40.5996
|
||||
c8.40039 -10.2002 7.10059 -25.3008 -3.09961 -33.8008c-10.6006 -8.89941 -25.7002 -6.69922 -33.7998 3c-24.8008 29.7002 -75 29.7002 -99.8008 0c-8.5 -10.1992 -23.5996 -11.5 -33.7998 -3s-11.5996 23.6006 -3.09961 33.8008
|
||||
c21.5996 25.7998 53.2002 40.5996 86.7998 40.5996zM200 208c0 -17.7002 -14.2998 -32.0996 -32 -32.0996s-32 14.2998 -32 32c0 6.19922 2.2002 11.6992 5.2998 16.5996l-28.2002 8.5c-12.6992 3.7998 -19.8994 17.2002 -16.0996 29.9004
|
||||
c3.7998 12.6992 17.0996 20 29.9004 16.0996l80 -24c12.6992 -3.7998 19.8994 -17.2002 16.0996 -29.9004c-3.09961 -10.3994 -12.7002 -17.0996 -23 -17.0996zM399 262.9c3.7998 -12.7002 -3.40039 -26.1006 -16.0996 -29.8008l-28.2002 -8.5
|
||||
c3.09961 -4.89941 5.2998 -10.3994 5.2998 -16.5996c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32c-10.2998 0 -19.9004 6.7002 -23 17.0996c-3.7998 12.7002 3.40039 26.1006 16.0996 29.9004l80 24c12.8008 3.7998 26.1006 -3.40039 29.9004 -16.0996z" />
|
||||
<glyph glyph-name="dizzy" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM214.2 209.9
|
||||
c-7.90039 -7.90039 -20.5 -7.90039 -28.4004 -0.200195l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998
|
||||
c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.9004c7.7998 7.7998 20.5 7.7998 28.2998 0s7.7998 -20.5 0 -28.2998l-17.9004 -17.9004l17.9004 -17.7998c7.7998 -7.7998 7.7998 -20.5 0 -28.2998zM374.2 302.1
|
||||
c7.7002 -7.7998 7.7002 -20.3994 0 -28.1992l-17.9004 -17.9004l17.7998 -18c7.80078 -7.7998 7.80078 -20.5 0 -28.2998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0l-17.7998 17.7998l-17.7998 -17.7998c-7.7998 -7.7998 -20.5 -7.7998 -28.2998 0
|
||||
c-7.80078 7.7998 -7.80078 20.5 0 28.2998l17.8994 17.9004l-17.8994 17.8994c-7.80078 7.7998 -7.80078 20.5 0 28.2998c7.7998 7.80078 20.5 7.80078 28.2998 0l17.7998 -17.7998l17.9004 17.7998c7.7998 7.80078 20.5 7.80078 28.2998 0zM248 176
|
||||
c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64s-64 28.7002 -64 64s28.7002 64 64 64z" />
|
||||
<glyph glyph-name="flushed" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM344 304c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80
|
||||
s-80 35.7998 -80 80s35.7998 80 80 80zM344 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM344 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM232 224c0 -44.2002 -35.7998 -80 -80 -80
|
||||
s-80 35.7998 -80 80s35.7998 80 80 80s80 -35.7998 80 -80zM152 176c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM152 248c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM312 104
|
||||
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
|
||||
<glyph glyph-name="frown-open" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32
|
||||
s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM248 160c35.5996 0 88.7998 -21.2998 95.7998 -61.2002c2 -11.7998 -9.09961 -21.5996 -20.5 -18.0996
|
||||
c-31.2002 9.59961 -59.3994 15.2998 -75.2998 15.2998s-44.0996 -5.7002 -75.2998 -15.2998c-11.5 -3.40039 -22.5 6.2998 -20.5 18.0996c7 39.9004 60.2002 61.2002 95.7998 61.2002z" />
|
||||
<glyph glyph-name="grimace" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
|
||||
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM344 192c26.5 0 48 -21.5 48 -48v-32c0 -26.5 -21.5 -48 -48 -48h-192c-26.5 0 -48 21.5 -48 48v32c0 26.5 21.5 48 48 48
|
||||
h192zM176 96v24h-40v-8c0 -8.7998 7.2002 -16 16 -16h24zM176 136v24h-24c-8.7998 0 -16 -7.2002 -16 -16v-8h40zM240 96v24h-48v-24h48zM240 136v24h-48v-24h48zM304 96v24h-48v-24h48zM304 136v24h-48v-24h48zM360 112v8h-40v-24h24c8.7998 0 16 7.2002 16 16zM360 136v8
|
||||
c0 8.7998 -7.2002 16 -16 16h-24v-24h40z" />
|
||||
<glyph glyph-name="grin" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
|
||||
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
|
||||
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z" />
|
||||
<glyph glyph-name="grin-alt" unicode="" horiz-adv-x="496"
|
||||
d="M200.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56
|
||||
c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM328.3 200c-7.5 -11.4004 -24.5996 -12 -32.7002 0c-12.3994 18.7002 -15.1992 37.2998 -15.6992 56c0.599609 18.7002 3.2998 37.2998 15.6992 56c7.60059 11.4004 24.7002 12 32.7002 0
|
||||
c12.4004 -18.7002 15.2002 -37.2998 15.7002 -56c-0.599609 -18.7002 -3.2998 -37.2998 -15.7002 -56zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200
|
||||
s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006
|
||||
s79.7002 4.7998 105.6 13.1006z" />
|
||||
<glyph glyph-name="grin-beam" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
|
||||
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM117.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
|
||||
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996
|
||||
l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002zM277.7 216.3c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998
|
||||
c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002z" />
|
||||
<glyph glyph-name="grin-beam-sweat" unicode="" horiz-adv-x="496"
|
||||
d="M440 288c-29.5 0 -53.2998 26.2998 -53.2998 58.7002c0 25 31.7002 75.5 46.2002 97.2998c3.5 5.2998 10.5996 5.2998 14.1992 0c14.5 -21.7998 46.2002 -72.2998 46.2002 -97.2998c0 -32.4004 -23.7998 -58.7002 -53.2998 -58.7002zM248 48
|
||||
c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM378.3 216.3
|
||||
c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
|
||||
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM483.6 269.2c8 -24.2998 12.4004 -50.2002 12.4004 -77.2002c0 -137 -111 -248 -248 -248s-248 111 -248 248s111 248 248 248
|
||||
c45.7002 0 88.4004 -12.5996 125.2 -34.2002c-10.9004 -21.5996 -15.5 -36.2002 -17.2002 -45.7002c-31.2002 20.1006 -68.2002 31.9004 -108 31.9004c-110.3 0 -200 -89.7002 -200 -200s89.7002 -200 200 -200s200 89.7002 200 200
|
||||
c0 22.5 -3.90039 44.0996 -10.7998 64.2998c0.399414 0 21.7998 -2.7998 46.3994 12.9004zM168 258.6c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998
|
||||
c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
|
||||
<glyph glyph-name="grin-hearts" unicode="" horiz-adv-x="496"
|
||||
d="M353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM200.8 192.3
|
||||
l-70.2002 18.1006c-20.3994 5.2998 -31.8994 27 -24.1992 47.1992c6.69922 17.7002 26.6992 26.7002 44.8994 22l7.10059 -1.89941l2 7.09961c5.09961 18.1006 22.8994 30.9004 41.5 27.9004c21.3994 -3.40039 34.3994 -24.2002 28.7998 -44.5l-19.4004 -69.9004
|
||||
c-1.2998 -4.5 -6 -7.2002 -10.5 -6zM389.6 257.6c7.7002 -20.1992 -3.7998 -41.7998 -24.1992 -47.0996l-70.2002 -18.2002c-4.60059 -1.2002 -9.2998 1.5 -10.5 6l-19.4004 69.9004c-5.59961 20.2998 7.40039 41.0996 28.7998 44.5c18.7002 3 36.5 -9.7998 41.5 -27.9004
|
||||
l2 -7.09961l7.10059 1.89941c18.2002 4.7002 38.2002 -4.39941 44.8994 -22zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200
|
||||
s89.7002 -200 200 -200z" />
|
||||
<glyph glyph-name="grin-squint" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
|
||||
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.9004 -123.3 80c-1.7002 9.90039 7.7998 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM118.9 184.2c-3.80078 4.39941 -3.90039 11 -0.100586 15.5l33.6006 40.2998
|
||||
l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5c3.89941 4.40039 10.1992 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5zM361.8 181.7
|
||||
l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5.10059 2.90039 11.5 1.90039 15.2998 -2.5c3.80078 -4.5 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5
|
||||
c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5z" />
|
||||
<glyph glyph-name="grin-squint-tears" unicode=""
|
||||
d="M117.1 63.9004c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998
|
||||
c16.8994 16.9004 75.0996 26.9004 100.899 30.6006zM75.9004 105.6c-19.6006 -3.89941 -35.1006 -8.09961 -47.3008 -12.1992c-39.2998 90.5996 -22.0996 199.899 52 274c48.5 48.3994 111.9 72.5996 175.4 72.5996c38.9004 0 77.7998 -9.2002 113.2 -27.4004
|
||||
c-4 -12.1992 -8.2002 -28 -12 -48.2998c-30.4004 17.9004 -65 27.7002 -101.2 27.7002c-53.4004 0 -103.6 -20.7998 -141.4 -58.5996c-61.5996 -61.5 -74.2998 -153.4 -38.6992 -227.801zM428.2 293.2c20.2998 3.89941 36.2002 8 48.5 12
|
||||
c47.8994 -93.2002 32.8994 -210.5 -45.2002 -288.601c-48.5 -48.3994 -111.9 -72.5996 -175.4 -72.5996c-33.6992 0 -67.2998 7 -98.6992 20.5996c4.19922 12.2002 8.2998 27.7002 12.1992 47.2002c26.6006 -12.7998 55.9004 -19.7998 86.4004 -19.7998
|
||||
c53.4004 0 103.6 20.7998 141.4 58.5996c65.6992 65.7002 75.7998 166 30.7998 242.601zM394.9 320.1c-6.30078 -0.899414 -11.7002 4.5 -10.9004 10.9004c3.7002 25.7998 13.7002 84 30.5996 100.9c22 21.8994 57.9004 21.5 80.3008 -0.900391
|
||||
c22.3994 -22.4004 22.7998 -58.4004 0.899414 -80.2998c-16.8994 -16.9004 -75.0996 -26.9004 -100.899 -30.6006zM207.9 211.8c3 -3 4.19922 -7.2998 3.19922 -11.5l-22.5996 -90.5c-1.40039 -5.39941 -6.2002 -9.09961 -11.7002 -9.09961h-0.899414
|
||||
c-5.80078 0.5 -10.5 5.09961 -11 10.8994l-4.80078 52.3008l-52.2998 4.7998c-5.7998 0.5 -10.3994 5.2002 -10.8994 11c-0.400391 5.89941 3.39941 11.2002 9.09961 12.5996l90.5 22.7002c4.2002 1 8.40039 -0.200195 11.4004 -3.2002zM247.6 236.9
|
||||
c-0.0996094 0 -6.39941 -1.80078 -11.3994 3.19922c-3 3 -4.2002 7.30078 -3.2002 11.4004l22.5996 90.5c1.40039 5.7002 7 9.2002 12.6006 9.09961c5.7998 -0.5 10.5 -5.09961 11 -10.8994l4.7998 -52.2998l52.2998 -4.80078c5.7998 -0.5 10.4004 -5.19922 10.9004 -11
|
||||
c0.399414 -5.89941 -3.40039 -11.1992 -9.10059 -12.5996zM299.6 148.4c29.1006 29.0996 53 59.5996 65.3008 83.7998c4.89941 9.2998 17.5996 9.89941 23.3994 1.7002c27.7002 -38.9004 6.10059 -106.9 -30.5996 -143.7s-104.8 -58.2998 -143.7 -30.6006
|
||||
c-8.2998 5.90039 -7.5 18.6006 1.7002 23.4004c24.2002 12.5 54.7998 36.2998 83.8994 65.4004z" />
|
||||
<glyph glyph-name="grin-stars" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM353.6 143.4c10 3.09961 19.3008 -5.5 17.7002 -15.3008
|
||||
c-8 -47.0996 -71.2998 -80 -123.3 -80s-115.4 32.8008 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.3008c26 -8.30078 64.3994 -13.1006 105.6 -13.1006s79.7002 4.7998 105.6 13.1006zM125.7 200.9l6.09961 34.8994l-25.3994 24.6006
|
||||
c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006l34.9004 -5c6.19922 -1 8.7998 -8.69922 4.2998 -13.1992l-25.4004 -24.6006l6 -34.8994c1 -6.2002 -5.39941 -11 -11 -7.90039
|
||||
l-31.2998 16.2998l-31.2998 -16.2998c-5.60059 -3.09961 -12 1.7002 -11 7.90039zM385.4 273.6c6.19922 -1 8.89941 -8.59961 4.39941 -13.1992l-25.3994 -24.6006l6 -34.8994c1 -6.2002 -5.40039 -11 -11 -7.90039l-31.3008 16.2998l-31.2998 -16.2998
|
||||
c-5.59961 -3.09961 -12 1.7002 -11 7.90039l6 34.8994l-25.3994 24.6006c-4.60059 4.59961 -1.90039 12.2998 4.2998 13.1992l34.8994 5l15.5 31.6006c2.90039 5.7998 11 5.7998 13.9004 0l15.5 -31.6006z" />
|
||||
<glyph glyph-name="grin-tears" unicode="" horiz-adv-x="640"
|
||||
d="M117.1 191.9c6.30078 0.899414 11.7002 -4.5 10.9004 -10.9004c-3.7002 -25.7998 -13.7002 -84 -30.5996 -100.9c-22 -21.8994 -57.9004 -21.5 -80.3008 0.900391c-22.3994 22.4004 -22.7998 58.4004 -0.899414 80.2998c16.8994 16.9004 75.0996 26.9004 100.899 30.6006
|
||||
zM623.8 161.3c21.9004 -21.8994 21.5 -57.8994 -0.799805 -80.2002c-22.4004 -22.3994 -58.4004 -22.7998 -80.2998 -0.899414c-16.9004 16.8994 -26.9004 75.0996 -30.6006 100.899c-0.899414 6.30078 4.5 11.7002 10.8008 10.8008
|
||||
c25.7998 -3.7002 84 -13.7002 100.899 -30.6006zM497.2 99.5996c12.3994 -37.2998 25.0996 -43.7998 28.2998 -46.5c-44.5996 -65.7998 -120 -109.1 -205.5 -109.1s-160.9 43.2998 -205.5 109.1c3.09961 2.60059 15.7998 9.10059 28.2998 46.5
|
||||
c33.4004 -63.8994 100.3 -107.6 177.2 -107.6s143.8 43.7002 177.2 107.6zM122.7 223.5c-2.40039 0.299805 -5 2.5 -49.5 -6.90039c12.3994 125.4 118.1 223.4 246.8 223.4s234.4 -98 246.8 -223.5c-44.2998 9.40039 -47.3994 7.2002 -49.5 7
|
||||
c-15.2002 95.2998 -97.7998 168.5 -197.3 168.5s-182.1 -73.2002 -197.3 -168.5zM320 48c-51.9004 0 -115.3 32.9004 -123.3 80c-1.7002 10 7.89941 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996
|
||||
c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-8 -47.0996 -71.3994 -80 -123.3 -80zM450.3 216.3c-3.09961 -0.899414 -7.2002 0.100586 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17
|
||||
c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998zM240 258.6
|
||||
c-12.2998 0 -23.7998 -7.7998 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
|
||||
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996z" />
|
||||
<glyph glyph-name="grin-tongue" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
|
||||
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
|
||||
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
|
||||
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32
|
||||
s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
|
||||
<glyph glyph-name="grin-tongue-squint" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0
|
||||
l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200
|
||||
s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996
|
||||
s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5l-33.6006 -40.2998
|
||||
l33.6006 -40.2998c3.7002 -4.5 3.7002 -11 -0.100586 -15.5c-3.59961 -4.2002 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 3 11.5 1.90039 15.2998 -2.5zM214.2 250.3
|
||||
c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998s-2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7002 -1.7002 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7002 4.5 -3.7002 11 0.100586 15.5
|
||||
c3.89941 4.5 10.2998 5.5 15.2998 2.5z" />
|
||||
<glyph glyph-name="grin-tongue-wink" unicode="" horiz-adv-x="496"
|
||||
d="M152 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
|
||||
c-3.7998 -3.39941 -9.2998 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM328 320c44.2002 0 80 -35.7998 80 -80s-35.7998 -80 -80 -80s-80 35.7998 -80 80s35.7998 80 80 80zM328 192
|
||||
c26.5 0 48 21.5 48 48s-21.5 48 -48 48s-48 -21.5 -48 -48s21.5 -48 48 -48zM328 264c13.2998 0 24 -10.7002 24 -24s-10.7002 -24 -24 -24s-24 10.7002 -24 24s10.7002 24 24 24zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248z
|
||||
M312 40h0.0996094v43.7998l-17.6992 8.7998c-15.1006 7.60059 -31.5 -1.69922 -34.9004 -16.5l-2.7998 -12.0996c-2.10059 -9.2002 -15.2002 -9.2002 -17.2998 0l-2.80078 12.0996c-3.39941 14.8008 -19.8994 24 -34.8994 16.5l-17.7002 -8.7998v-42.7998
|
||||
c0 -35.2002 28 -64.5 63.0996 -65c35.8008 -0.5 64.9004 28.4004 64.9004 64zM340.2 14.7002c64 33.3994 107.8 100.3 107.8 177.3c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -77 43.7998 -143.9 107.8 -177.3
|
||||
c-2.2002 8.09961 -3.7998 16.5 -3.7998 25.2998v43.5c-14.2002 12.4004 -24.4004 27.5 -27.2998 44.5c-1.7002 10 7.7998 18.4004 17.7002 15.2998c26 -8.2998 64.3994 -13.0996 105.6 -13.0996s79.7002 4.7998 105.6 13.0996c10 3.2002 19.4004 -5.39941 17.7002 -15.2998
|
||||
c-2.89941 -17 -13.0996 -32.0996 -27.2998 -44.5v-43.5c0 -8.7998 -1.59961 -17.2002 -3.7998 -25.2998z" />
|
||||
<glyph glyph-name="grin-wink" unicode="" horiz-adv-x="496"
|
||||
d="M328 268c25.6904 0 55.8799 -16.9199 59.8701 -42.1201c1.72949 -11.0898 -11.3506 -18.2695 -19.8301 -10.8398l-9.5498 8.47949c-14.8105 13.1904 -46.1602 13.1904 -60.9707 0l-9.5498 -8.47949c-8.33008 -7.40039 -21.5801 -0.379883 -19.8301 10.8398
|
||||
c3.98047 25.2002 34.1699 42.1201 59.8604 42.1201zM168 208c-17.6699 0 -32 14.3301 -32 32s14.3301 32 32 32s32 -14.3301 32 -32s-14.3301 -32 -32 -32zM353.55 143.36c10.04 3.13965 19.3906 -5.4502 17.71 -15.3408
|
||||
c-7.92969 -47.1494 -71.3193 -80.0195 -123.26 -80.0195s-115.33 32.8701 -123.26 80.0195c-1.69043 9.9707 7.76953 18.4707 17.71 15.3408c25.9297 -8.31055 64.3994 -13.0605 105.55 -13.0605s79.6201 4.75977 105.55 13.0605zM248 440c136.97 0 248 -111.03 248 -248
|
||||
s-111.03 -248 -248 -248s-248 111.03 -248 248s111.03 248 248 248zM248 -8c110.28 0 200 89.7197 200 200s-89.7197 200 -200 200s-200 -89.7197 -200 -200s89.7197 -200 200 -200z" />
|
||||
<glyph glyph-name="kiss" unicode="" horiz-adv-x="496"
|
||||
d="M168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
|
||||
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
|
||||
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM248 440c137 0 248 -111 248 -248
|
||||
s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z
|
||||
" />
|
||||
<glyph glyph-name="kiss-beam" unicode="" horiz-adv-x="496"
|
||||
d="M168 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
|
||||
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004zM248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8
|
||||
c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM304 140c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5
|
||||
c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002
|
||||
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM328 296
|
||||
c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.299805 -3.7998 -2 -7.19922 -5.59961 -8.2998c-3.10059 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996c-12.3008 0 -23.8008 -7.89941 -31.5 -21.5996l-9.5 -17
|
||||
c-1.80078 -3.2002 -5.80078 -4.7002 -9.30078 -3.7002c-3.59961 1.10059 -5.89941 4.60059 -5.59961 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
|
||||
<glyph glyph-name="kiss-wink-heart" unicode="" horiz-adv-x="504"
|
||||
d="M304 139.5c0 -13 -13.4004 -27.2998 -35.0996 -36.4004c21.7998 -8.69922 35.1992 -23 35.1992 -36c0 -19.1992 -28.6992 -41.5 -71.5 -44h-0.5c-3.69922 0 -7 2.60059 -7.7998 6.2002c-0.899414 3.7998 1.10059 7.7002 4.7002 9.2002l17 7.2002
|
||||
c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.2002c-6 2.59961 -5.7002 12.3994 0 14.7998l17 7.2002c12.9004 5.5 20.7002 13.5 20.7002 21.5s-7.7998 16 -20.7998 21.5l-16.9004 7.19922c-3.59961 1.5 -5.59961 5.40039 -4.7002 9.2002
|
||||
c0.799805 3.7998 4.40039 6.60059 8.2002 6.2002c42.7002 -2.5 71.5 -24.7998 71.5 -44zM374.5 223c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-2.5 -2.2998 -7.90039 -4.7002 -13.7002 -1.59961c-4.39941 2.39941 -6.89941 7.39941 -6.09961 12.3994
|
||||
c3.89941 25.2002 34.2002 42.1006 59.7998 42.1006s55.7998 -16.9004 59.7998 -42.1006c0.799805 -5 -1.7002 -10 -6.09961 -12.3994c-4.40039 -2.40039 -9.90039 -1.7002 -13.7002 1.59961zM136 239.5c0 17.7002 14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32
|
||||
s-32 14.2998 -32 32zM501.1 45.5c9.2002 -23.9004 -4.39941 -49.4004 -28.5 -55.7002l-83 -21.5c-5.39941 -1.39941 -10.8994 1.7998 -12.3994 7.10059l-22.9004 82.5996c-6.59961 24 8.7998 48.5996 34 52.5996c22 3.5 43.1006 -11.5996 49 -33l2.2998 -8.39941
|
||||
l8.40039 2.2002c21.5996 5.59961 45.0996 -5.10059 53.0996 -25.9004zM334 11.7002c17.7002 -64 10.9004 -39.5 13.4004 -46.7998c-30.5 -13.4004 -64 -20.9004 -99.4004 -20.9004c-137 0 -248 111 -248 248s111 248 248 248s248 -111 247.9 -248
|
||||
c0 -31.7998 -6.2002 -62.0996 -17.1006 -90c-6 1.5 -12.2002 2.7998 -18.5996 2.90039c-5.60059 9.69922 -13.6006 17.5 -22.6006 23.8994c6.7002 19.9004 10.4004 41.1006 10.4004 63.2002c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200
|
||||
c30.7998 0 59.9004 7.2002 86 19.7002z" />
|
||||
<glyph glyph-name="laugh" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
|
||||
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM168 224
|
||||
c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
|
||||
<glyph glyph-name="laugh-beam" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
|
||||
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 296c23.7998 0 52.7002 -29.2998 55.7998 -71.4004c0.700195 -8.5 -10.7998 -11.8994 -14.8994 -4.5
|
||||
l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006c-12.3008 0 -23.8008 -7.90039 -31.5 -21.6006l-9.5 -17c-4.10059 -7.39941 -15.6006 -4.09961 -14.9004 4.5c3.2998 42.1006 32.2002 71.4004 56 71.4004zM127 220.1c-4.2002 -7.39941 -15.7002 -4 -15.0996 4.5
|
||||
c3.2998 42.1006 32.1992 71.4004 56 71.4004c23.7998 0 52.6992 -29.2998 56 -71.4004c0.699219 -8.5 -10.8008 -11.8994 -14.9004 -4.5l-9.5 17c-7.7002 13.7002 -19.2002 21.6006 -31.5 21.6006s-23.7998 -7.90039 -31.5 -21.6006zM362.4 160c8.19922 0 14.5 -7 13.5 -15
|
||||
c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
|
||||
<glyph glyph-name="laugh-squint" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
|
||||
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM343.6 252l33.6006 -40.2998c8.59961 -10.4004 -3.90039 -24.7998 -15.4004 -18l-80 48
|
||||
c-7.7998 4.7002 -7.7998 15.8994 0 20.5996l80 48c11.6006 6.7998 24 -7.7002 15.4004 -18zM134.2 193.7c-11.6006 -6.7998 -24.1006 7.59961 -15.4004 18l33.6006 40.2998l-33.6006 40.2998c-8.59961 10.2998 3.7998 24.9004 15.4004 18l80 -48
|
||||
c7.7998 -4.7002 7.7998 -15.8994 0 -20.5996zM362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
|
||||
<glyph glyph-name="laugh-wink" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM389.4 50.5996c37.7998 37.8008 58.5996 88 58.5996 141.4s-20.7998 103.6 -58.5996 141.4c-37.8008 37.7998 -88 58.5996 -141.4 58.5996s-103.6 -20.7998 -141.4 -58.5996
|
||||
c-37.7998 -37.8008 -58.5996 -88 -58.5996 -141.4s20.7998 -103.6 58.5996 -141.4c37.8008 -37.7998 88 -58.5996 141.4 -58.5996s103.6 20.7998 141.4 58.5996zM328 284c25.7002 0 55.9004 -16.9004 59.7002 -42.0996c1.7998 -11.1006 -11.2998 -18.2002 -19.7998 -10.8008
|
||||
l-9.5 8.5c-14.8008 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5c-8.30078 -7.39941 -21.5 -0.399414 -19.8008 10.8008c4 25.1992 34.2002 42.0996 59.9004 42.0996zM168 224c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32s-14.2998 -32 -32 -32z
|
||||
M362.4 160c8.19922 0 14.5 -7 13.5 -15c-7.5 -59.2002 -58.9004 -105 -121.101 -105h-13.5996c-62.2002 0 -113.601 45.7998 -121.101 105c-1 8 5.30078 15 13.5 15h228.801z" />
|
||||
<glyph glyph-name="meh-blank" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM168 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32
|
||||
s-32 14.2998 -32 32s14.2998 32 32 32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
|
||||
<glyph glyph-name="meh-rolling-eyes" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM336 296c39.7998 0 72 -32.2002 72 -72s-32.2002 -72 -72 -72
|
||||
s-72 32.2002 -72 72s32.2002 72 72 72zM336 184c22.0996 0 40 17.9004 40 40c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998
|
||||
c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998c0 -22.0996 17.9004 -40 40 -40zM232 224c0 -39.7998 -32.2002 -72 -72 -72s-72 32.2002 -72 72s32.2002 72 72 72s72 -32.2002 72 -72zM120 224c0 -22.0996 17.9004 -40 40 -40s40 17.9004 40 40
|
||||
c0 13.5996 -7.2998 25.0996 -17.7002 32.2998c1 -2.59961 1.7002 -5.39941 1.7002 -8.2998c0 -13.2998 -10.7002 -24 -24 -24s-24 10.7002 -24 24c0 3 0.700195 5.7002 1.7002 8.2998c-10.4004 -7.2002 -17.7002 -18.7002 -17.7002 -32.2998zM312 96
|
||||
c13.2002 0 24 -10.7998 24 -24s-10.7998 -24 -24 -24h-128c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24h128z" />
|
||||
<glyph glyph-name="sad-cry" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM392 53.5996c34.5996 35.9004 56 84.7002 56 138.4c0 110.3 -89.7002 200 -200 200s-200 -89.7002 -200 -200c0 -53.7002 21.4004 -102.4 56 -138.4v114.4
|
||||
c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-151.4c28.5 -15.5996 61.2002 -24.5996 96 -24.5996s67.5 9 96 24.5996v151.4c0 13.2002 10.7998 24 24 24s24 -10.7998 24 -24v-114.4zM205.8 213.5c-5.7998 -3.2002 -11.2002 -0.700195 -13.7002 1.59961l-9.5 8.5
|
||||
c-14.7998 13.2002 -46.1992 13.2002 -61 0l-9.5 -8.5c-3.7998 -3.39941 -9.2998 -4 -13.6992 -1.59961c-4.40039 2.40039 -6.90039 7.40039 -6.10059 12.4004c3.90039 25.1992 34.2002 42.0996 59.7998 42.0996c25.6006 0 55.8008 -16.9004 59.8008 -42.0996
|
||||
c0.799805 -5 -1.7002 -10 -6.10059 -12.4004zM344 268c25.7002 0 55.9004 -16.9004 59.7998 -42.0996c0.799805 -5 -1.7002 -10 -6.09961 -12.4004c-5.7002 -3.09961 -11.2002 -0.599609 -13.7002 1.59961l-9.5 8.5c-14.7998 13.2002 -46.2002 13.2002 -61 0l-9.5 -8.5
|
||||
c-3.7998 -3.39941 -9.2002 -4 -13.7002 -1.59961c-4.39941 2.40039 -6.89941 7.40039 -6.09961 12.4004c3.89941 25.1992 34.0996 42.0996 59.7998 42.0996zM248 176c30.9004 0 56 -28.7002 56 -64s-25.0996 -64 -56 -64s-56 28.7002 -56 64s25.0996 64 56 64z" />
|
||||
<glyph glyph-name="sad-tear" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM256 144c38.0996 0 74 -16.7998 98.5 -46.0996
|
||||
c8.5 -10.2002 7.09961 -25.3008 -3.09961 -33.8008c-10.6006 -8.7998 -25.7002 -6.69922 -33.8008 3.10059c-15.2998 18.2998 -37.7998 28.7998 -61.5996 28.7998c-13.2002 0 -24 10.7998 -24 24s10.7998 24 24 24zM168 208c-17.7002 0 -32 14.2998 -32 32s14.2998 32 32 32
|
||||
s32 -14.2998 32 -32s-14.2998 -32 -32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32zM162.4 173.2c2.7998 3.7002 8.39941 3.7002 11.1992 0c11.4004 -15.2998 36.4004 -50.6006 36.4004 -68.1006
|
||||
c0 -22.6992 -18.7998 -41.0996 -42 -41.0996s-42 18.4004 -42 41.0996c0 17.5 25 52.8008 36.4004 68.1006z" />
|
||||
<glyph glyph-name="smile-beam" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM332 135.4c8.5 10.1992 23.5996 11.5 33.7998 3.09961
|
||||
c10.2002 -8.5 11.6006 -23.5996 3.10059 -33.7998c-30 -36 -74.1006 -56.6006 -120.9 -56.6006s-90.9004 20.6006 -120.9 56.6006c-8.39941 10.2002 -7.09961 25.2998 3.10059 33.7998c10.2002 8.40039 25.2998 7.09961 33.7998 -3.09961
|
||||
c20.7998 -25.1006 51.5 -39.4004 84 -39.4004s63.2002 14.4004 84 39.4004zM136.5 237l-9.5 -17c-1.90039 -3.2002 -5.90039 -4.7998 -9.2998 -3.7002c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004s52.7002 -29.2998 56 -71.4004
|
||||
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996zM328 296c23.7998 0 52.7002 -29.2998 56 -71.4004
|
||||
c0.299805 -3.7998 -2.09961 -7.19922 -5.7002 -8.2998c-3.09961 -1 -7.2002 0 -9.2998 3.7002l-9.5 17c-7.7002 13.7002 -19.2002 21.5996 -31.5 21.5996s-23.7998 -7.89941 -31.5 -21.5996l-9.5 -17c-1.90039 -3.2002 -5.7998 -4.7998 -9.2998 -3.7002
|
||||
c-3.60059 1.10059 -6 4.60059 -5.7002 8.2998c3.2998 42.1006 32.2002 71.4004 56 71.4004z" />
|
||||
<glyph glyph-name="surprise" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM248 168c35.2998 0 64 -28.7002 64 -64s-28.7002 -64 -64 -64
|
||||
s-64 28.7002 -64 64s28.7002 64 64 64zM200 240c0 -17.7002 -14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32s32 -14.2998 32 -32zM328 272c17.7002 0 32 -14.2998 32 -32s-14.2998 -32 -32 -32s-32 14.2998 -32 32s14.2998 32 32 32z" />
|
||||
<glyph glyph-name="tired" unicode="" horiz-adv-x="496"
|
||||
d="M248 440c137 0 248 -111 248 -248s-111 -248 -248 -248s-248 111 -248 248s111 248 248 248zM248 -8c110.3 0 200 89.7002 200 200s-89.7002 200 -200 200s-200 -89.7002 -200 -200s89.7002 -200 200 -200zM377.1 295.8c3.80078 -4.39941 3.90039 -11 0.100586 -15.5
|
||||
l-33.6006 -40.2998l33.6006 -40.2998c3.7998 -4.5 3.7002 -11 -0.100586 -15.5c-3.5 -4.10059 -9.89941 -5.7002 -15.2998 -2.5l-80 48c-3.59961 2.2002 -5.7998 6.09961 -5.7998 10.2998s2.2002 8.09961 5.7998 10.2998l80 48c5 2.90039 11.5 1.90039 15.2998 -2.5z
|
||||
M220 240c0 -4.2002 -2.2002 -8.09961 -5.7998 -10.2998l-80 -48c-5.40039 -3.2002 -11.7998 -1.60059 -15.2998 2.5c-3.80078 4.5 -3.90039 11 -0.100586 15.5l33.6006 40.2998l-33.6006 40.2998c-3.7998 4.5 -3.7002 11 0.100586 15.5
|
||||
c3.7998 4.40039 10.2998 5.5 15.2998 2.5l80 -48c3.59961 -2.2002 5.7998 -6.09961 5.7998 -10.2998zM248 176c45.4004 0 100.9 -38.2998 107.8 -93.2998c1.5 -11.9004 -7 -21.6006 -15.5 -17.9004c-22.7002 9.7002 -56.2998 15.2002 -92.2998 15.2002
|
||||
s-69.5996 -5.5 -92.2998 -15.2002c-8.60059 -3.7002 -17 6.10059 -15.5 17.9004c6.89941 55 62.3994 93.2998 107.8 93.2998z" />
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
After Width: | Height: | Size: 141 KiB |
BIN
app/public/static/fa/webfonts/fa-regular-400.ttf
Normal file
BIN
app/public/static/fa/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
app/public/static/fa/webfonts/fa-regular-400.woff
Normal file
BIN
app/public/static/fa/webfonts/fa-regular-400.woff
Normal file
Binary file not shown.
BIN
app/public/static/fa/webfonts/fa-regular-400.woff2
Normal file
BIN
app/public/static/fa/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
app/public/static/fa/webfonts/fa-solid-900.eot
Normal file
BIN
app/public/static/fa/webfonts/fa-solid-900.eot
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user