forked from platypush/platypush
Compare commits
772 commits
Author | SHA1 | Date | |
---|---|---|---|
82e796e20b | |||
197158fa36 | |||
21c0787138 | |||
f511e2eaf1 | |||
eae57003b5 | |||
c8b6b1fcdd | |||
cdab6aab3c | |||
5a32d94991 | |||
899d137df7 | |||
ac11185af7 | |||
575c0ab730 | |||
010b52ed19 | |||
61b2afce91 | |||
66a585e653 | |||
acaca67c61 | |||
171efec739 | |||
225761c839 | |||
457c5cb3e3 | |||
fd07709811 | |||
197e1e91dd | |||
ad59ff8e4d | |||
d3247a96a0 | |||
6199991084 | |||
55118119d0 | |||
3e02304ac2 | |||
|
29284907ca | ||
|
a1fcbc8ca4 | ||
|
8ebae0086e | ||
|
0b12fd7fe5 | ||
|
032572a869 | ||
697a260026 | |||
dce2861021 | |||
cc2548e159 | |||
2dee5a25ec | |||
2410d892f5 | |||
4d8ad87e42 | |||
40849b1502 | |||
9629e04211 | |||
771912c448 | |||
2bf61e37fa | |||
a7cb15f67d | |||
27ee490264 | |||
cd2b0a2666 | |||
2fc91ba4f6 | |||
559063ed90 | |||
46321559ab | |||
38756119c4 | |||
b887122a7f | |||
8bc5e5b8c5 | |||
3680a9b1d1 | |||
d80bb9a476 | |||
a99bde7014 | |||
d799d50391 | |||
2df88c1911 | |||
478e4afb0d | |||
df580c2907 | |||
459fe9427c | |||
f47dbc842a | |||
34aea3cd02 | |||
9999025c0a | |||
1230236ca5 | |||
b646b5f3d7 | |||
bc42ba16d7 | |||
b967cb1969 | |||
7c7b80c942 | |||
09413bc0cc | |||
c3766ee423 | |||
dc3e896d51 | |||
65da4bb33f | |||
|
e33a2c1a70 | ||
7147af7f3b | |||
7b64b35f53 | |||
c28745e710 | |||
cfce4e11e2 | |||
845e40fd95 | |||
53817413a4 | |||
4836c6d036 | |||
c1416fdcf9 | |||
a4a56fdbee | |||
227fdf4271 | |||
bb02e7c9e4 | |||
81bf0e9561 | |||
5c1aeed30c | |||
6dacddad97 | |||
4759da30eb | |||
d15d744017 | |||
3e54d5d7b3 | |||
43e88c71c6 | |||
71bafec11e | |||
2883c4f086 | |||
e2382c5ea2 | |||
dd02be12fc | |||
1cb42f8923 | |||
6936a67182 | |||
13856365fc | |||
f98f9c3b96 | |||
1626fc737e | |||
c04aaba2bc | |||
2b7df634fc | |||
8feee6ce39 | |||
5fac324b43 | |||
3dab232d33 | |||
11e25a79a5 | |||
e73872b65e | |||
c96e83107e | |||
2c05b7a225 | |||
84a5eeb86a | |||
3543052c11 | |||
fe3d3d6c16 | |||
bd7644b7cc | |||
83ced6a320 | |||
47728d5bbd | |||
89bcdbe1ac | |||
8fa2080652 | |||
25b77f068b | |||
76b86aec0e | |||
897f3a6f47 | |||
8e9fb65db5 | |||
51464b808c | |||
825593a445 | |||
ec8fe401d2 | |||
3a9d5700ea | |||
d171795e7c | |||
4015cf356d | |||
fa942b06e3 | |||
4373d4ceaa | |||
7620e1ead7 | |||
e314a7bca9 | |||
3e61bd7a9b | |||
12c800b7a9 | |||
7b8d92b120 | |||
3ffb061e2a | |||
9716b1da35 | |||
e30ae16ef7 | |||
86559a623a | |||
4f81a73fb9 | |||
117dfad64e | |||
9b99c1e19d | |||
585c2f733f | |||
cad864f220 | |||
e3e3638ffe | |||
c605a65bf1 | |||
f032957d0b | |||
f364be17e3 | |||
0aae905754 | |||
0ab160569a | |||
9cadf98d52 | |||
44557e812f | |||
14b25ac891 | |||
e16247529e | |||
9ebe251d46 | |||
2308c4e927 | |||
9e6ae42660 | |||
c88a6aa3e6 | |||
9e78a9a297 | |||
1a53c59382 | |||
bf82ad9bf0 | |||
|
0d62760036 | ||
|
1b735779d3 | ||
841516d9de | |||
c134f29c72 | |||
dc104a9c8e | |||
afdacc90d6 | |||
117d3f07d9 | |||
b44d21d31f | |||
f13e00f22e | |||
54e3703462 | |||
a7f03a1af9 | |||
63d9c1e348 | |||
9244eedc49 | |||
bc01cddee0 | |||
f81c0dad4c | |||
f42ed34f75 | |||
54a4c21882 | |||
c48efe07bd | |||
e1547da794 | |||
62737b5a95 | |||
8d8e1878bb | |||
393493550c | |||
38c1ebb90c | |||
419c227ff8 | |||
d1da4803cf | |||
b6c0029208 | |||
8b621cbf30 | |||
e5a2127ecb | |||
91c66cdd48 | |||
f6b3d34eff | |||
839948e4e6 | |||
0d0665ca7c | |||
6dd1d481d5 | |||
|
6d11f8d2ef | ||
|
5a3b6c3c07 | ||
|
0758dcae92 | ||
0e40d77bc7 | |||
be8140ddb5 | |||
e7e76087c0 | |||
dfbbea93fd | |||
ab07fc0fa3 | |||
771e32e368 | |||
156a6379d0 | |||
c4610254f8 | |||
853fce2521 | |||
b337bf7a53 | |||
152c2020de | |||
daa030ff4c | |||
471ec1370c | |||
202cff093f | |||
6eb8b7954d | |||
0bc714d0e3 | |||
1e9f7fb2c6 | |||
946c7b1783 | |||
5a7068501a | |||
efe2bb6196 | |||
05b1fcd43a | |||
f18d0d8b74 | |||
15cf611c95 | |||
bbfc5b32e6 | |||
1133c6019a | |||
1316af9553 | |||
44e319e7ca | |||
4e5c740908 | |||
cc621cdca6 | |||
b0d9a95331 | |||
485a1db3d3 | |||
e17abc34c1 | |||
b74b8aa154 | |||
d623b3d1b8 | |||
68e3cc51e4 | |||
dd2ea2092e | |||
3249053eb0 | |||
bbc70fe6e6 | |||
9ec21fe10d | |||
c54269e3d2 | |||
e39e36e5f6 | |||
c5c872eb68 | |||
26f491025a | |||
90a953b738 | |||
981ae3479e | |||
bca340ebc1 | |||
9aff704444 | |||
5061c5290c | |||
c68cf4b585 | |||
b53c4c5c18 | |||
e01782c344 | |||
6df699359c | |||
861e7e7c52 | |||
1369848114 | |||
9d086a4a10 | |||
7cc7009d08 | |||
a3eedc6adc | |||
740e35bd5e | |||
457333929f | |||
e593264eab | |||
cb9244964c | |||
62980cc9e2 | |||
42d672ab5e | |||
ef524fa388 | |||
dea72fbfdb | |||
99909c73ab | |||
1ee8055597 | |||
d9916873cb | |||
234963b069 | |||
ffc3fe218d | |||
0f186c44ef | |||
06781cd72c | |||
24f7d4a789 | |||
c788f2d858 | |||
f6b1f92a88 | |||
9f8fe60cdf | |||
83d21d3f04 | |||
54a6b34a64 | |||
377b2c2425 | |||
a152b0d734 | |||
496dfdb50b | |||
9493445af6 | |||
0657c80a5c | |||
e672a7fb5c | |||
e8acf8615f | |||
336cb18cb3 | |||
342df0eeec | |||
e6a358fe27 | |||
818f60a468 | |||
db34a607e4 | |||
8f2e68f0db | |||
8b3c2a8ee1 | |||
92bff4decb | |||
0bb264792e | |||
a5426ede58 | |||
2c481c54af | |||
0010342fb7 | |||
1e9418b072 | |||
213498318f | |||
077e12e9a8 | |||
897e8a9ff7 | |||
b439b8b0f4 | |||
f1c640fabb | |||
666bbe5372 | |||
9dfb22c23a | |||
2b48edfabc | |||
6e27c9b8e4 | |||
74a2958ff4 | |||
8333cc09ee | |||
01571e2e65 | |||
a21aaee888 | |||
5080caa38e | |||
ca5853cbab | |||
1f120b167b | |||
|
09412acba7 | ||
|
846324fa12 | ||
af21ff13ff | |||
e0ff180fb0 | |||
1189e71539 | |||
50beb1460b | |||
d155094729 | |||
866be36aad | |||
129c7587ad | |||
d70737ea2b | |||
b8b70f43c0 | |||
8b6c1fb969 | |||
0071fc54b3 | |||
ee1d91db6b | |||
d0f65e84e0 | |||
4d18345cda | |||
4c80e6fd34 | |||
598de6b91a | |||
20483524d1 | |||
9bbdfc1eb9 | |||
4568b4659e | |||
54c2264403 | |||
52d92da907 | |||
88cc18de92 | |||
fb99eefe40 | |||
ba390ab2f3 | |||
6053a80796 | |||
ccc778e056 | |||
e10f1d7e1b | |||
de96b4ea17 | |||
92fe119cff | |||
f809ce0cb0 | |||
0963cd3d55 | |||
a70f151e28 | |||
b71612cb2f | |||
dfe0092857 | |||
90736f8ffb | |||
6cd342e1f4 | |||
e234210fb5 | |||
179c8265cf | |||
c13623c3f7 | |||
a8343cb45b | |||
91f6beb349 | |||
683ffa98c1 | |||
6b5dbe7c1e | |||
7351a2685a | |||
79dc5e238d | |||
a11f17aa8f | |||
67d8d0a515 | |||
8ec1ca8543 | |||
2cbb005c67 | |||
cf813e4197 | |||
b1b51b4b7e | |||
70db33b4e2 | |||
357d92b479 | |||
2033f9760a | |||
ee27b2c4c6 | |||
8904e40f9f | |||
fe2497577d | |||
01aedb5568 | |||
2ccf00508d | |||
8329de15ba | |||
c1b1bd6c50 | |||
|
9a388afd1d | ||
|
f63dd68043 | ||
|
40c5a69a57 | ||
|
5070b4dad5 | ||
|
d2e51e7ee2 | ||
fdd42b8a22 | |||
baa584c1ca | |||
929271ee0e | |||
f608475380 | |||
|
5a1c6b064a | ||
f2b2189282 | |||
3e8ab8d0db | |||
3f195b4e9c | |||
e75a865c86 | |||
af187e95e5 | |||
255422d5b8 | |||
18a7b05a80 | |||
0c8d86b8d4 | |||
6ab0c3d953 | |||
48996622d9 | |||
38edaaf311 | |||
15b615efe8 | |||
6e99e87aa6 | |||
a5c69d2ded | |||
7661d9c843 | |||
9e36c5550f | |||
8291a97cd9 | |||
579f9816e2 | |||
b69e950076 | |||
3fddf67949 | |||
4441461354 | |||
52f5ce2642 | |||
392d64d03b | |||
4d39791569 | |||
fabcba14d9 | |||
38cf102397 | |||
6d425b06f7 | |||
234c3ce7d8 | |||
28ce11d636 | |||
490ed4c361 | |||
b039d98c66 | |||
4c4e29b34e | |||
f55cacb2e3 | |||
35c827ca4a | |||
b312f1717f | |||
a27955a583 | |||
0e012c9800 | |||
1d873aca05 | |||
8c0943e700 | |||
dc96b4995c | |||
837b0fad98 | |||
03950e23f7 | |||
1f0aa0965d | |||
01af85d024 | |||
c128887c3e | |||
673a52fde5 | |||
cf5993ede0 | |||
6f8a81f020 | |||
71ff453587 | |||
484959a153 | |||
329296b606 | |||
b8d8b48d73 | |||
398925d76e | |||
b44bd0be32 | |||
5ebdb381f1 | |||
aa92db9850 | |||
e710a3a974 | |||
c95381cead | |||
c5ac02d133 | |||
910304b817 | |||
e8723eae98 | |||
061e5a67a2 | |||
a746273f73 | |||
e180c9c76f | |||
c416d0ea1f | |||
75aed6af92 | |||
ef4d0bd38c | |||
bd01827b52 | |||
f64d47565d | |||
79ba8deb71 | |||
84e06e30fe | |||
f78027a6eb | |||
b43c4612fd | |||
dce6096020 | |||
96aa22c03e | |||
16527417da | |||
1ad68cac11 | |||
972f9dffb9 | |||
6a0f19a62f | |||
81fb1a47c3 | |||
24b5b3ba14 | |||
21ac87394a | |||
55c4f5797b | |||
c7f12e0bd8 | |||
5ff839919c | |||
7266fe8a43 | |||
c8fa53e62f | |||
e65bf99baf | |||
9b42815d77 | |||
7c610413df | |||
aaf6c39255 | |||
a4979f1513 | |||
ded64e8dc2 | |||
1a9ac56923 | |||
12d53b846e | |||
5e905db0f5 | |||
|
17dbd91198 | ||
|
e70f12b948 | ||
|
e59606e588 | ||
98a6adb7ef | |||
d4354e81f8 | |||
c9daa037a7 | |||
701623c99d | |||
8880b966fc | |||
5d4bfb3f90 | |||
5f23aa8e78 | |||
8cea668e37 | |||
26d9e6d9b9 | |||
b890326e71 | |||
b2b59d651c | |||
b83dee50e5 | |||
773986f211 | |||
705ba82fa1 | |||
affe95be96 | |||
6faa845afd | |||
c7ee97bb0b | |||
5799a2b352 | |||
1a21671dde | |||
1f544c9e53 | |||
a754b06f88 | |||
3dc1ff3c6e | |||
1774e464cc | |||
1dd905dc66 | |||
ec050b2853 | |||
9993e9b6b7 | |||
5672b23fbe | |||
744da20b7c | |||
91e2530dd5 | |||
8fc3201b8c | |||
|
9027eaf4d1 | ||
|
dedfc0bc3a | ||
|
8b65a5f151 | ||
|
d8598f60a2 | ||
36f49952c4 | |||
dff6aeec6d | |||
|
d6ab2ee02b | ||
|
2e08e2f820 | ||
ed697c0ad2 | |||
22cfe777fa | |||
3f2832a077 | |||
6f8eb397d2 | |||
3163721bf3 | |||
d79b8a1de5 | |||
3afc6b2271 | |||
17b6b02986 | |||
87a902bfa3 | |||
421feffd3e | |||
518dc146d6 | |||
6b11db7afb | |||
8814859abc | |||
0ccd029ff1 | |||
e52f5e06f4 | |||
|
066d71faa3 | ||
4f19b45975 | |||
7d6ffc76fb | |||
7a8f30e5e0 | |||
3528b3646f | |||
9cca928d4b | |||
f356fcd844 | |||
fcae7aa3ad | |||
c7d640a1d2 | |||
1cc2aaf5a4 | |||
2acf6ef3e9 | |||
1107e526f7 | |||
5fc9c1199b | |||
b067430cd5 | |||
ff60896625 | |||
67b6e3a608 | |||
c61a1b89d6 | |||
c9a5c29a4a | |||
1067ab04d9 | |||
709b90fa4b | |||
06f0ac4545 | |||
944fd45f9f | |||
6acdde6164 | |||
3583dafbc3 | |||
4513bb9569 | |||
4e82dd17bb | |||
e982c02524 | |||
d9a5ea1e53 | |||
23e02de1d7 | |||
4d0b63a155 | |||
ce1525e786 | |||
67478e7ca1 | |||
c18768e61f | |||
30362b89e3 | |||
826a3fa55c | |||
3986549326 | |||
fa318882a5 | |||
d6185ddb1e | |||
|
fee12951d6 | ||
d2caa989ac | |||
fa3c804b71 | |||
4cd0761e78 | |||
16f7b7e12a | |||
22222fab65 | |||
5b3c0ad1cf | |||
3758a8d759 | |||
de2bbc53c6 | |||
a4a776986b | |||
9fef73a746 | |||
0f6f119089 | |||
c64ff40dd3 | |||
5c0f85c311 | |||
e6702398dc | |||
983bcc240a | |||
d6d9d7a8e7 | |||
8d26721040 | |||
96f265a4a2 | |||
0ffff854d3 | |||
020804fd1c | |||
42174b31bc | |||
3642d1ffa4 | |||
104457a302 | |||
0445087699 | |||
751d719b04 | |||
bef027fc07 | |||
cc670f9d4a | |||
86674ddc28 | |||
ee3933dc77 | |||
e23664b5e7 | |||
0537815721 | |||
a2ec20bb3a | |||
d3562f4d20 | |||
bf5aece08b | |||
2f20580498 | |||
1911c05afe | |||
cac256af08 | |||
a784a6fe23 | |||
0baae01ab7 | |||
f70d352cd7 | |||
7c7e488867 | |||
baee33b88f | |||
6de0b2e041 | |||
35751da068 | |||
0479c37d64 | |||
3a4d7afcf0 | |||
e071e99dab | |||
2e0246413c | |||
b1dd484704 | |||
303b11613b | |||
05c6449d8b | |||
5e52741986 | |||
7d8a00696c | |||
254604e404 | |||
8f0002ae40 | |||
8e05a7f4c9 | |||
0fd2992894 | |||
efd97f7186 | |||
3ccdd4d179 | |||
d20cd4b058 | |||
c49b4ca273 | |||
5c2204f99d | |||
cbc58c7330 | |||
7ca2159acb | |||
2dd5b6c122 | |||
a51d8978e7 | |||
a5adac9314 | |||
a211e2e2e4 | |||
7fa0dbda7b | |||
|
46da373637 | ||
4038ef3bc1 | |||
1cad0394ab | |||
02a3385638 | |||
c899627eca | |||
d0f781919d | |||
86b4b14112 | |||
32b8296244 | |||
2ab1743bec | |||
70ffe0ec04 | |||
3d5c60e4f4 | |||
f06233801b | |||
59c693d6a0 | |||
79a71d00b4 | |||
c77cf2c98b | |||
f514f7ce1e | |||
98a98ea1dc | |||
77c91aa5e3 | |||
0e11bbeb05 | |||
20f3eaf375 | |||
55e230c361 | |||
|
092f5b607c | ||
ad4d929c28 | |||
7ae99b4325 | |||
2efffb8ebe | |||
15b1c1f3c0 | |||
6003b205c8 | |||
825f20ab77 | |||
7d87c5e92a | |||
3b3f157086 | |||
ad20345505 | |||
fc4d006064 | |||
e10fb4e549 | |||
404737dc24 | |||
2896bd64b6 | |||
7e8f8a35fc | |||
b662e98447 | |||
f978d708cb | |||
6f8c2085f2 | |||
|
6f0451b733 | ||
|
c80af03e8d | ||
|
186e85cc86 | ||
|
83ca9fd8e5 | ||
|
8437f05d6a | ||
8d04eadd77 | |||
e74137d4d1 | |||
6b12d5ca0e | |||
e683912228 | |||
912dddd3da | |||
afbb61565b | |||
df8299ab61 | |||
741f1aef84 | |||
3df76a4a9c | |||
929ac09cae | |||
3277c56b43 | |||
c906aab64d | |||
2021e25752 | |||
13bde4adba | |||
579faf63bc | |||
7a849379f9 | |||
|
4a100b0dc0 | ||
|
5a1bccac4d | ||
|
499b3cd9c9 | ||
|
8be6d80b45 | ||
|
fc621a27f3 | ||
85e44542e2 | |||
c094fac7dc | |||
9dc0fbe6e7 | |||
d6b1337c5a | |||
0adde5a107 | |||
89ced918f1 | |||
bf52f44f92 | |||
e285009da3 | |||
3edc8d1077 | |||
cbaca5f303 | |||
df79bb3ea6 | |||
228031c4ad | |||
fdeba9e53c | |||
3c88593e9a | |||
6a8c83f99b | |||
901338e228 | |||
|
9651354fbf | ||
a5826892dd | |||
373d98fc6a | |||
61e466d8e2 | |||
4ac73837f6 | |||
72bc697122 | |||
b2c07a31f2 | |||
d813356d9c | |||
1192782729 | |||
4734909912 | |||
237fc58928 | |||
d8e24207c5 | |||
bd4b1d3e0f | |||
a373091c64 | |||
632d98703b | |||
6f8816d23d | |||
af1392b5b9 | |||
bb9b6cd319 | |||
f0a780b759 | |||
f04f7ce9d7 | |||
37d70d67ac | |||
fa1d5eb672 | |||
a345b12244 | |||
2c197c275e | |||
9de49c71a1 | |||
a6f7b6e790 | |||
aa333db05c | |||
af875c996e | |||
a4c911a5d7 | |||
8193c5702c | |||
8378bee7c6 | |||
a670f01647 | |||
921025be0c | |||
bb38f2439c | |||
f7517eb321 | |||
a9498ea191 | |||
44f9c03bf3 | |||
98c99c7888 | |||
e123463804 | |||
f99f6bdab9 | |||
4972c8bdcf | |||
33d4c8342d | |||
027bcea612 | |||
|
749eda16b4 | ||
|
2d72ce9645 | ||
|
8ed358fcc8 | ||
|
a30042e1eb | ||
|
b9d637187c | ||
|
6a55527694 | ||
|
5488c97f4b | ||
|
e12054c484 |
1361 changed files with 44059 additions and 14045 deletions
931
.drone.yml
931
.drone.yml
File diff suppressed because it is too large
Load diff
46
.drone/build-ui.sh
Executable file
46
.drone/build-ui.sh
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
export SRCDIR="$PWD"
|
||||||
|
export WEBAPP_DIR="$SRCDIR/platypush/backend/http/webapp"
|
||||||
|
export SKIPCI="$PWD/.skipci"
|
||||||
|
rm -rf "$SKIPCI"
|
||||||
|
|
||||||
|
. .drone/macros/configure-git.sh
|
||||||
|
|
||||||
|
cd "$WEBAPP_DIR"
|
||||||
|
if [ $(git log --pretty=oneline $DRONE_COMMIT_AFTER...$DRONE_COMMIT_BEFORE . | wc -l) -eq 0 ]; then
|
||||||
|
echo "No UI changes detected, skipping build"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$(git log --pretty=format:%s HEAD...HEAD~1 | head -1)" == "[Automatic] Updated UI files" ]; then
|
||||||
|
echo "UI changes have already been committed, skipping build"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf dist node_modules
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
if [ $(git status --porcelain dist | wc -l) -eq 0 ]; then
|
||||||
|
echo "No build files have been changed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a .skipci file to mark the fact that the next steps should be skipped
|
||||||
|
# (we're going to do another push anyway, so another pipeline will be triggered)
|
||||||
|
touch "$SKIPCI"
|
||||||
|
cd "$SRCDIR"
|
||||||
|
|
||||||
|
. .drone/macros/configure-ssh.sh
|
||||||
|
. .drone/macros/configure-gpg.sh
|
||||||
|
|
||||||
|
git add "${WEBAPP_DIR}/dist"
|
||||||
|
git commit "${WEBAPP_DIR}/dist" -S -m "[Automatic] Updated UI files" --no-verify
|
||||||
|
git remote rm origin
|
||||||
|
git remote add origin git@git.platypush.tech:platypush/platypush.git
|
||||||
|
git push -f origin master
|
||||||
|
|
||||||
|
# Restore the original git configuration
|
||||||
|
mv "$TMP_GIT_CONF" "$GIT_CONF"
|
23
.drone/github-mirror.sh
Executable file
23
.drone/github-mirror.sh
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. .drone/macros/configure-git.sh
|
||||||
|
. .drone/macros/configure-ssh.sh
|
||||||
|
|
||||||
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
# Clone the repository
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
if [ -z "${branch}" ]; then
|
||||||
|
echo "No branch checked out"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git remote add github git@github.com:/blacklight/platypush.git
|
||||||
|
|
||||||
|
if [[ "$branch" == "master" ]]; then
|
||||||
|
git pull --rebase github "${branch}" || echo "No such branch on Github"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Push the changes to the GitHub mirror
|
||||||
|
git push -f --all -v github
|
||||||
|
git push --tags -v github
|
29
.drone/macros/configure-git.sh
Executable file
29
.drone/macros/configure-git.sh
Executable file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Install git
|
||||||
|
if [ -z "$(which git)" ]; then
|
||||||
|
if [ -n "$(which apt-get)" ]; then
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y git
|
||||||
|
elif [ -n "$(which apk)" ]; then
|
||||||
|
apk add --update --no-cache git
|
||||||
|
elif [ -n "$(which yum)" ]; then
|
||||||
|
yum install -y git
|
||||||
|
elif [ -n "$(which dnf)" ]; then
|
||||||
|
dnf install -y git
|
||||||
|
elif [ -n "$(which pacman)" ]; then
|
||||||
|
pacman -Sy --noconfirm git
|
||||||
|
else
|
||||||
|
echo "Could not find a package manager to install git"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup the original git configuration before changing attributes
|
||||||
|
export GIT_CONF="$PWD/.git/config"
|
||||||
|
export TMP_GIT_CONF=/tmp/git.config.orig
|
||||||
|
cp "$GIT_CONF" "$TMP_GIT_CONF"
|
||||||
|
|
||||||
|
git config --global --add safe.directory "$PWD"
|
||||||
|
git config user.name "Platypush CI/CD Automation"
|
||||||
|
git config user.email "admin@platypush.tech"
|
30
.drone/macros/configure-gpg.sh
Executable file
30
.drone/macros/configure-gpg.sh
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -z "$PGP_KEY" ] && echo "PGP_KEY is not set" && exit 1
|
||||||
|
[ -z "$PGP_KEY_ID" ] && echo "PGP_KEY_ID is not set" && exit 1
|
||||||
|
|
||||||
|
# Install gpg
|
||||||
|
if [ -z "$(which gpg)" ]; then
|
||||||
|
if [ -n "$(which apt-get)" ]; then
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y gnupg
|
||||||
|
elif [ -n "$(which apk)" ]; then
|
||||||
|
apk add --update --no-cache bash gnupg
|
||||||
|
elif [ -n "$(which yum)" ]; then
|
||||||
|
yum install -y gnupg
|
||||||
|
elif [ -n "$(which dnf)" ]; then
|
||||||
|
dnf install -y gnupg
|
||||||
|
elif [ -n "$(which pacman)" ]; then
|
||||||
|
pacman -Sy --noconfirm gnupg
|
||||||
|
else
|
||||||
|
echo "Could not find a package manager to install gnupg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF | gpg --import --armor
|
||||||
|
$PGP_KEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git config commit.gpgsign true
|
||||||
|
git config user.signingkey "$PGP_KEY_ID"
|
35
.drone/macros/configure-ssh.sh
Executable file
35
.drone/macros/configure-ssh.sh
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ -z "$SSH_PUBKEY" ] || [ -z "$SSH_PRIVKEY" ]; then
|
||||||
|
echo "SSH_PUBKEY and SSH_PRIVKEY environment variables must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install ssh
|
||||||
|
if [ -z "$(which ssh)" ]; then
|
||||||
|
if [ -n "$(which apt-get)" ]; then
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y openssh
|
||||||
|
elif [ -n "$(which apk)" ]; then
|
||||||
|
apk add --update --no-cache openssh
|
||||||
|
elif [ -n "$(which yum)" ]; then
|
||||||
|
yum install -y openssh
|
||||||
|
elif [ -n "$(which dnf)" ]; then
|
||||||
|
dnf install -y openssh
|
||||||
|
elif [ -n "$(which pacman)" ]; then
|
||||||
|
pacman -Sy --noconfirm openssh
|
||||||
|
else
|
||||||
|
echo "Could not find a package manager to install openssh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo $SSH_PUBKEY > ~/.ssh/id_rsa.pub
|
||||||
|
|
||||||
|
cat <<EOF > ~/.ssh/id_rsa
|
||||||
|
$SSH_PRIVKEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan git.platypush.tech >> ~/.ssh/known_hosts 2>/dev/null
|
27
.drone/rebuild-docs.sh
Executable file
27
.drone/rebuild-docs.sh
Executable file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Installing required build dependencies"
|
||||||
|
apk add --update --no-cache git make py3-sphinx py3-myst-parser py3-pip $(cat platypush/install/requirements/alpine.txt)
|
||||||
|
pip install -U sphinx-rtd-theme sphinx-book-theme --break-system-packages
|
||||||
|
pip install . --break-system-packages
|
||||||
|
mkdir -p /docs/current
|
||||||
|
export APPDIR="$PWD"
|
||||||
|
rm -rf "$APPDIR/docs/build"
|
||||||
|
|
||||||
|
echo "Building the updated documentation"
|
||||||
|
cd "$APPDIR/docs/source"
|
||||||
|
git clone 'https://git.platypush.tech/platypush/platypush.wiki.git' wiki
|
||||||
|
|
||||||
|
echo "Linking the wiki to the Sphinx index"
|
||||||
|
cd wiki
|
||||||
|
cd "$APPDIR/docs"
|
||||||
|
make html
|
||||||
|
rm -f config*.yaml
|
||||||
|
cd "$APPDIR"
|
||||||
|
|
||||||
|
echo "Copying the new documentation files to the target folder"
|
||||||
|
mv -v "$APPDIR/docs/build" /docs/new
|
||||||
|
cd /docs
|
||||||
|
mv current old
|
||||||
|
mv new current
|
||||||
|
rm -rf old
|
6
.drone/run-tests.sh
Executable file
6
.drone/run-tests.sh
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
apk add --update --no-cache $(cat platypush/install/requirements/alpine.txt)
|
||||||
|
pip install . --break-system-packages
|
||||||
|
pip install -r requirements-tests.txt --break-system-packages
|
||||||
|
pytest tests
|
169
.drone/update-apt-repo.sh
Executable file
169
.drone/update-apt-repo.sh
Executable file
|
@ -0,0 +1,169 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -f .skipci ] && exit 0
|
||||||
|
|
||||||
|
echo "-- Installing dependencies"
|
||||||
|
apt update
|
||||||
|
apt install -y dpkg-dev gpg
|
||||||
|
|
||||||
|
echo "-- Creating a new apt root folder"
|
||||||
|
export TMP_APT_ROOT="/tmp/apt"
|
||||||
|
cp -r "$APT_ROOT" "$TMP_APT_ROOT"
|
||||||
|
|
||||||
|
echo "-- Cleaning up older apt releases"
|
||||||
|
|
||||||
|
find "$TMP_APT_ROOT/pool" -mindepth 2 -maxdepth 2 -type d | while read reldir; do
|
||||||
|
pkg_to_remove=$(( $(ls "$reldir"/*.deb | wc -l) - 1 ))
|
||||||
|
[ $pkg_to_remove -le 0 ] && continue
|
||||||
|
ls "$reldir"/*.deb | sort -V | head -n$pkg_to_remove | xargs rm -f
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "-- Updating Packages files"
|
||||||
|
|
||||||
|
echo "stable\noldstable\nubuntu" | while read distro; do
|
||||||
|
echo "main\ndev" | while read branch; do
|
||||||
|
branch_dir="$TMP_APT_ROOT/pool/$distro/$branch"
|
||||||
|
echo "Checking pool folder: $branch_dir"
|
||||||
|
[ -d "$branch_dir" ] || mkdir -p "$branch_dir"
|
||||||
|
dist_dir="$TMP_APT_ROOT/dists/$distro/$branch/all"
|
||||||
|
mkdir -p "$dist_dir"
|
||||||
|
pkg_file="$dist_dir/Packages"
|
||||||
|
dpkg-scanpackages --arch all "$branch_dir" > "$pkg_file"
|
||||||
|
sed -i "$pkg_file" -re "s|^Filename: $TMP_APT_ROOT/|Filename: |"
|
||||||
|
cat "$pkg_file" | gzip -9 > "$pkg_file.gz"
|
||||||
|
echo "Generated Packages file: $pkg_file"
|
||||||
|
cat "$pkg_file"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "-- Updating Release files"
|
||||||
|
|
||||||
|
add_hashes() {
|
||||||
|
dist_dir=$1
|
||||||
|
hash_cmd=$2
|
||||||
|
hash_label=$3
|
||||||
|
|
||||||
|
echo "$hash_label:"
|
||||||
|
find "$dist_dir" -name 'Packages*' | while read file; do
|
||||||
|
basename="$(echo "$file" | sed -r -e "s|^$dist_dir/||")"
|
||||||
|
hash="$($hash_cmd "$file" | cut -d" " -f1)"
|
||||||
|
size="$(wc -c < $file)"
|
||||||
|
echo " $hash $size $basename"
|
||||||
|
echo " $hash $size $(echo $basename | sed -re 's|/all/|/binary-i386/|')"
|
||||||
|
echo " $hash $size $(echo $basename | sed -re 's|/all/|/binary-amd64/|')"
|
||||||
|
echo " $hash $size $(echo $basename | sed -re 's|/all/|/binary-armel/|')"
|
||||||
|
echo " $hash $size $(echo $basename | sed -re 's|/all/|/binary-armhf/|')"
|
||||||
|
echo " $hash $size $(echo $basename | sed -re 's|/all/|/binary-arm64/|')"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "stable\noldstable\nubuntu" | while read distro; do
|
||||||
|
dist_dir="$TMP_APT_ROOT/dists/$distro"
|
||||||
|
components=$(find "$dist_dir" -name Packages | awk -F '/' '{print $(NF-2)}' | uniq | tr '\n' ' ')
|
||||||
|
release_file="$dist_dir/Release"
|
||||||
|
|
||||||
|
cat <<EOF > "$release_file"
|
||||||
|
Origin: Platypush repository
|
||||||
|
Label: Platypush
|
||||||
|
Suite: $distro
|
||||||
|
Codename: $distro
|
||||||
|
Architectures: i386 amd64 armel armhf arm64
|
||||||
|
Components: $components
|
||||||
|
Description: The official APT repository for Platypush
|
||||||
|
Date: $(date -Ru)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
add_hashes "$dist_dir" "md5sum" "MD5Sum" >> "$release_file"
|
||||||
|
add_hashes "$dist_dir" "sha1sum" "SHA1" >> "$release_file"
|
||||||
|
add_hashes "$dist_dir" "sha256sum" "SHA256" >> "$release_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "-- Generating list files"
|
||||||
|
mkdir -p "$TMP_APT_ROOT/lists"
|
||||||
|
|
||||||
|
for distro in stable oldstable ubuntu; do
|
||||||
|
for branch in main dev; do
|
||||||
|
echo "deb https://apt.platypush.tech/ $distro $branch" > "$TMP_APT_ROOT/lists/platypush-$distro-$branch.list"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "-- Updating index file"
|
||||||
|
|
||||||
|
cat <<EOF > "$TMP_APT_ROOT/index.txt"
|
||||||
|
Welcome to the Platypush APT repository!
|
||||||
|
|
||||||
|
Project homepage: https://platypush.tech
|
||||||
|
Source code: https://git.platypush.tech/platypush/platypush
|
||||||
|
Documentation / API reference: https://docs.platypush.tech
|
||||||
|
|
||||||
|
You can use this APT repository to install Platypush on Debian, Ubuntu or any
|
||||||
|
Debian-based distro.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Add this repository's PGP key to your apt keyring
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
# wget -q -O \\\
|
||||||
|
/etc/apt/trusted.gpg.d/platypush.asc \\\
|
||||||
|
https://apt.platypush.tech/pubkey.txt
|
||||||
|
|
||||||
|
2. Add the repository to your sources
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
# wget -q -O \\\
|
||||||
|
/etc/apt/sources.list.d/platypush.list \\\
|
||||||
|
https://apt.platypush.tech/lists/platypush-<deb_version>-<branch>.list
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
- deb_version can be:
|
||||||
|
- *stable* - current Debian stable version
|
||||||
|
- *oldstable* - previous Debian stable version
|
||||||
|
- *ubuntu* - latest Ubuntu version
|
||||||
|
|
||||||
|
- branch can be either:
|
||||||
|
- *main* - latest stable release
|
||||||
|
- *dev* a package always in sync with the git version
|
||||||
|
|
||||||
|
For example, to install the latest stable tags on Debian stable:
|
||||||
|
|
||||||
|
# wget -q -O \\\
|
||||||
|
/etc/apt/sources.list.d/platypush.list \\\
|
||||||
|
https://apt.platypush.tech/lists/platypush-stable-main.list
|
||||||
|
|
||||||
|
3. Update your repos
|
||||||
|
====================
|
||||||
|
|
||||||
|
# apt update
|
||||||
|
|
||||||
|
4. Install Platypush
|
||||||
|
====================
|
||||||
|
|
||||||
|
# apt install platypush
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "-- Importing and refreshing PGP key"
|
||||||
|
echo "$PGP_PUBKEY" > "$TMP_APT_ROOT/pubkey.txt"
|
||||||
|
export PGP_KEYID=$(echo "$PGP_PUBKEY" | gpg --with-colons --import-options show-only --import --fingerprint | grep -e '^fpr:' | head -1 | awk -F ':' '{print $(NF - 1)}')
|
||||||
|
|
||||||
|
cat <<EOF | gpg --import --armor
|
||||||
|
$PGP_PRIVKEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "-- Signing Release files"
|
||||||
|
|
||||||
|
find "$TMP_APT_ROOT/dists" -type f -name Release | while read file; do
|
||||||
|
dirname="$(dirname "$file")"
|
||||||
|
cat "$file" | gpg -q --default-key "$PGP_KEYID" -abs > "$file.gpg"
|
||||||
|
cat "$file" | gpg -q --default-key "$PGP_KEYID" -abs --clearsign > "$dirname/InRelease"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "-- Updating the apt repo root"
|
||||||
|
export OLD_APT_ROOT="$REPOS_ROOT/oldapt"
|
||||||
|
rm -rf "$OLD_APT_ROOT"
|
||||||
|
mv "$APT_ROOT" "$OLD_APT_ROOT"
|
||||||
|
mv "$TMP_APT_ROOT" "$APT_ROOT"
|
||||||
|
|
||||||
|
chmod -R a+r "$APT_ROOT"
|
||||||
|
chmod a+x "$APT_ROOT"
|
38
.drone/update-arch-git-package.sh
Executable file
38
.drone/update-arch-git-package.sh
Executable file
|
@ -0,0 +1,38 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -f .skipci ] && exit 0
|
||||||
|
|
||||||
|
apk add --update --no-cache curl pacman sudo
|
||||||
|
|
||||||
|
. .drone/macros/configure-ssh.sh
|
||||||
|
. .drone/macros/configure-git.sh
|
||||||
|
|
||||||
|
git pull --rebase origin master --tags
|
||||||
|
|
||||||
|
export VERSION=$(python setup.py --version)
|
||||||
|
export HEAD=$(git log --pretty=format:%h HEAD...HEAD~1 | head -1)
|
||||||
|
export GIT_VERSION="$VERSION.r$(git log --pretty=oneline HEAD...v$VERSION | wc -l).g${HEAD}"
|
||||||
|
|
||||||
|
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
adduser -u 1000 -D build
|
||||||
|
mkdir -p "$WORKDIR"
|
||||||
|
|
||||||
|
echo "--- Updating Arch git version"
|
||||||
|
export PKGDIR=$WORKDIR/git
|
||||||
|
git clone ssh://aur@aur.archlinux.org/platypush-git.git "$PKGDIR"
|
||||||
|
git config --global --add safe.directory "$PKGDIR"
|
||||||
|
chown -R build "$PKGDIR"
|
||||||
|
cd "$PKGDIR"
|
||||||
|
|
||||||
|
sed -i 'PKGBUILD' -r \
|
||||||
|
-e "s/^pkgver=.*/pkgver=$GIT_VERSION/" \
|
||||||
|
-e "s/^pkgrel=.*/pkgrel=1/" \
|
||||||
|
|
||||||
|
sudo -u build makepkg --printsrcinfo > .SRCINFO
|
||||||
|
export FILES_CHANGED=$(git status --porcelain --untracked-files=no | wc -l)
|
||||||
|
|
||||||
|
if [ $FILES_CHANGED -gt 0 ]; then
|
||||||
|
echo "--- Pushing git package version $GIT_VERSION"
|
||||||
|
git commit -a -m '[Automatic] Package update'
|
||||||
|
git push origin master
|
||||||
|
fi
|
46
.drone/update-arch-stable-package.sh
Executable file
46
.drone/update-arch-stable-package.sh
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -f .skipci ] && exit 0
|
||||||
|
|
||||||
|
apk add --update --no-cache curl pacman sudo
|
||||||
|
|
||||||
|
. .drone/macros/configure-ssh.sh
|
||||||
|
. .drone/macros/configure-git.sh
|
||||||
|
|
||||||
|
git pull --rebase origin master --tags
|
||||||
|
|
||||||
|
export VERSION=$(python setup.py --version)
|
||||||
|
export TAG_URL="https://git.platypush.tech/platypush/platypush/archive/v$VERSION.tar.gz"
|
||||||
|
|
||||||
|
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
adduser -u 1000 -D build
|
||||||
|
mkdir -p "$WORKDIR"
|
||||||
|
|
||||||
|
echo "--- Updating Arch stable version"
|
||||||
|
export PKGDIR="$WORKDIR/stable"
|
||||||
|
git clone ssh://aur@aur.archlinux.org/platypush.git "$PKGDIR"
|
||||||
|
git config --global --add safe.directory "$PKGDIR"
|
||||||
|
chown -R build "$PKGDIR"
|
||||||
|
cd "$PKGDIR"
|
||||||
|
export RELEASED_VERSION=$(grep -e '^pkgver=' PKGBUILD | sed -r -e 's/^pkgver=(.*)\s*/\1/')
|
||||||
|
|
||||||
|
if [ "$RELEASED_VERSION" == "$VERSION" ]; then
|
||||||
|
echo "--- No changes in the stable package version"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
export TAG_CHECKSUM=$(curl --silent "$TAG_URL" | sha512sum | awk '{print $1}')
|
||||||
|
|
||||||
|
sed -i 'PKGBUILD' -r \
|
||||||
|
-e "s/^pkgver=.*/pkgver=$VERSION/" \
|
||||||
|
-e "s/^pkgrel=.*/pkgrel=1/" \
|
||||||
|
-e "s/^sha512sums=.*/sha512sums=('$TAG_CHECKSUM')/"
|
||||||
|
|
||||||
|
sudo -u build makepkg --printsrcinfo > .SRCINFO
|
||||||
|
export FILES_CHANGED=$(git status --porcelain --untracked-files=no | wc -l)
|
||||||
|
|
||||||
|
if [ $FILES_CHANGED -gt 0 ]; then
|
||||||
|
echo "--- Pushing stable package version $VERSION"
|
||||||
|
git commit -a -m '[Automatic] Package update'
|
||||||
|
git push origin master
|
||||||
|
fi
|
46
.drone/update-components-cache.sh
Executable file
46
.drone/update-components-cache.sh
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
export SKIPCI="$PWD/.skipci"
|
||||||
|
export CACHEFILE="$PWD/platypush/components.json.gz"
|
||||||
|
|
||||||
|
[ -f "$SKIPCI" ] && exit 0
|
||||||
|
|
||||||
|
# Backup the original git configuration before changing attributes
|
||||||
|
export GIT_CONF="$PWD/.git/config"
|
||||||
|
export TMP_GIT_CONF="/tmp/git.config.orig"
|
||||||
|
cp "$GIT_CONF" "$TMP_GIT_CONF"
|
||||||
|
|
||||||
|
. .drone/macros/configure-git.sh
|
||||||
|
|
||||||
|
# Only regenerate the components cache if either the plugins, backends,
|
||||||
|
# events or schemas folders have some changes (excluding the webapp files).
|
||||||
|
if [ -z "$(git log --pretty=oneline $DRONE_COMMIT_AFTER...$DRONE_COMMIT_BEFORE -- platypush/backend platypush/plugins platypush/schemas platypush/message/event ':(exclude)platypush/backend/http/webapp')" ]; then
|
||||||
|
echo 'No changes to the components file'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
. .drone/macros/configure-ssh.sh
|
||||||
|
. .drone/macros/configure-gpg.sh
|
||||||
|
|
||||||
|
echo 'Updating components cache'
|
||||||
|
apk add --update --no-cache $(cat platypush/install/requirements/alpine.txt)
|
||||||
|
pip install . --break-system-packages
|
||||||
|
|
||||||
|
python - <<EOF
|
||||||
|
from platypush import get_plugin
|
||||||
|
|
||||||
|
get_plugin('inspect').refresh_cache(force=True)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create a .skipci file to mark the fact that the next steps should be skipped
|
||||||
|
# (we're going to do another push anyway, so another pipeline will be triggered)
|
||||||
|
touch "$SKIPCI"
|
||||||
|
|
||||||
|
git add "$CACHEFILE"
|
||||||
|
git commit "$CACHEFILE" -S -m "[Automatic] Updated components cache" --no-verify
|
||||||
|
git remote rm origin
|
||||||
|
git remote add origin git@git.platypush.tech:platypush/platypush.git
|
||||||
|
git push -f origin master
|
||||||
|
|
||||||
|
# Restore the original git configuration
|
||||||
|
mv "$TMP_GIT_CONF" "$GIT_CONF"
|
103
.drone/update-deb-packages.sh
Executable file
103
.drone/update-deb-packages.sh
Executable file
|
@ -0,0 +1,103 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -f .skipci ] && exit 0
|
||||||
|
|
||||||
|
echo "-- Copying source directory"
|
||||||
|
mkdir -p "$WORKDIR/src"
|
||||||
|
export SRCDIR="$WORKDIR/src/$DEB_VERSION"
|
||||||
|
cp -r "$PWD" "$SRCDIR"
|
||||||
|
cd "$SRCDIR"
|
||||||
|
|
||||||
|
echo "-- Installing dependencies"
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt update
|
||||||
|
apt install -y curl dpkg-dev gpg git python3 python3-pip python3-setuptools
|
||||||
|
|
||||||
|
echo "--- Parsing metadata"
|
||||||
|
git config --global --add safe.directory "$PWD"
|
||||||
|
git pull --rebase origin master --tags
|
||||||
|
export VERSION=$(grep -e '^__version__' "${SRCDIR}/version.py" | sed -r -e 's/^__version__\s*=\s*"([^"]+)"$/\1/')
|
||||||
|
export GIT_VERSION="$VERSION-$(git log --pretty=oneline HEAD...v$VERSION | wc -l)"
|
||||||
|
export GIT_BUILD_DIR="$WORKDIR/${PKG_NAME}_${GIT_VERSION}_all"
|
||||||
|
export GIT_DEB="$WORKDIR/${PKG_NAME}_${GIT_VERSION}_all.deb"
|
||||||
|
export POOL_PATH="$APT_ROOT/pool/$DEB_VERSION/dev"
|
||||||
|
|
||||||
|
echo "--- Building git package"
|
||||||
|
pip install --prefix="$GIT_BUILD_DIR/usr" --no-cache --no-deps .
|
||||||
|
|
||||||
|
find "$GIT_BUILD_DIR" -name "site-packages" | while read dir; do
|
||||||
|
base="$(dirname "$dir")"
|
||||||
|
mv "$dir" "$base/dist-packages"
|
||||||
|
done
|
||||||
|
|
||||||
|
install -m755 -d "${GIT_BUILD_DIR}/usr/lib/systemd/system"
|
||||||
|
install -m755 -d "${GIT_BUILD_DIR}/usr/lib/systemd/user"
|
||||||
|
install -m750 -d "${GIT_BUILD_DIR}/var/lib/platypush"
|
||||||
|
install -m750 -d "${GIT_BUILD_DIR}/etc/platypush/scripts"
|
||||||
|
|
||||||
|
install -m644 "${SRCDIR}/platypush/config/config.yaml" "${GIT_BUILD_DIR}/etc/platypush/config.yaml"
|
||||||
|
install -m644 "${SRCDIR}/platypush/config/systemd/platypush.service" "${GIT_BUILD_DIR}/usr/lib/systemd/user/platypush.service"
|
||||||
|
install -m644 "${SRCDIR}/platypush/config/systemd/platypush.service" "${GIT_BUILD_DIR}/usr/lib/systemd/system/platypush.service"
|
||||||
|
sed -i "${GIT_BUILD_DIR}/usr/lib/systemd/system/platypush.service" -r \
|
||||||
|
-e 's/^#\s*Requires=(.*)/Requires=\1/' \
|
||||||
|
-e 's/^\[Service\]$/\[Service\]\
|
||||||
|
User=platypush\
|
||||||
|
Group=platypush\
|
||||||
|
WorkingDirectory=\/var\/lib\/platypush\
|
||||||
|
Environment="PLATYPUSH_CONFIG=\/etc\/platypush\/config.yaml"\
|
||||||
|
Environment="PLATYPUSH_WORKDIR=\/var\/lib\/platypush"/'
|
||||||
|
|
||||||
|
mkdir -p "$GIT_BUILD_DIR/DEBIAN"
|
||||||
|
|
||||||
|
cat <<EOF > "$GIT_BUILD_DIR/DEBIAN/control"
|
||||||
|
Package: $PKG_NAME
|
||||||
|
Version: $GIT_VERSION
|
||||||
|
Maintainer: Fabio Manganiello <fabio@platypush.tech>
|
||||||
|
Depends: $(cat platypush/install/requirements/debian.txt | tr '\n' ',' | sed -re 's/,$//' -e 's/,/, /g')
|
||||||
|
Architecture: all
|
||||||
|
Homepage: https://platypush.tech
|
||||||
|
Description: Universal command executor and automation hub.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > "$GIT_BUILD_DIR/DEBIAN/postinst" && chmod +x "$GIT_BUILD_DIR/DEBIAN/postinst"
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "\$1" = "configure" ]; then
|
||||||
|
grep -e '^platypush:' /etc/passwd 2>/dev/null || useradd -U -r -s /bin/false -d /var/lib/platypush platypush
|
||||||
|
mkdir -p /var/lib/platypush
|
||||||
|
chown -R platypush:platypush /var/lib/platypush
|
||||||
|
chown -R platypush:platypush /etc/platypush
|
||||||
|
if which systemctl; then systemctl daemon-reload; fi
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "$POOL_PATH"
|
||||||
|
rm -f "$POOL_PATH/"*.deb
|
||||||
|
dpkg --build "$GIT_BUILD_DIR"
|
||||||
|
|
||||||
|
echo "--- Copying $GIT_DEB to $POOL_PATH"
|
||||||
|
cp "$GIT_DEB" "$POOL_PATH"
|
||||||
|
|
||||||
|
# If main/all/Packages doesn't exist, then we should create the first main release
|
||||||
|
[ $(ls "$APT_ROOT/pool/$DEB_VERSION/main/${PKG_NAME}_${VERSION}-"*"_all.deb" 2>/dev/null | wc -l) -eq 0 ] && export UPDATE_STABLE_PKG=1
|
||||||
|
|
||||||
|
export PKGURL="https://apt.platypush.tech/dists/$DEB_VERSION/main/all/Packages"
|
||||||
|
|
||||||
|
[ -z "$UPDATE_STABLE_PKG" ] &&
|
||||||
|
curl -ILs -o /dev/null -w "%{http_code}" "$PKGURL" |
|
||||||
|
grep -e '^4' >/dev/null && export UPDATE_STABLE_PKG=1
|
||||||
|
|
||||||
|
# If the published release version differs from the current one, then we should publish a new main release
|
||||||
|
if [ -z "$UPDATE_STABLE_PKG" ]; then
|
||||||
|
RELEASED_VERSION=$(curl -s "$PKGURL" | grep -e '^Version: ' | head -1 | awk '{print $2}' | cut -d- -f 1)
|
||||||
|
[ "$RELEASED_VERSION" != "$VERSION" ] && export UPDATE_STABLE_PKG=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Proceed and update the main release if the version number has changed
|
||||||
|
if [ -n "$UPDATE_STABLE_PKG" ]; then
|
||||||
|
echo "--- Updating main package"
|
||||||
|
mkdir -p "$APT_ROOT/pool/$DEB_VERSION/main"
|
||||||
|
cp "$GIT_DEB" "$APT_ROOT/pool/$DEB_VERSION/main/${PKG_NAME}_${VERSION}-1_all.deb"
|
||||||
|
fi
|
26
.drone/update-image-registry.sh
Executable file
26
.drone/update-image-registry.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -z "$DOCKER_USER" ] && echo "Please set the DOCKER_USER environment variable" && exit 1
|
||||||
|
[ -z "$DOCKER_PASS" ] && echo "Please set the DOCKER_PASS environment variable" && exit 1
|
||||||
|
|
||||||
|
export VERSION=$(grep current_version pyproject.toml | sed -r -e "s/.*=\s*['\"]?([^'\"]+)['\"]?\s*$/\1/")
|
||||||
|
export REGISTRY_ENDPOINT="${REGISTRY_ENDPOINT:-quay.io}"
|
||||||
|
export IMAGE_NAME="$REGISTRY_ENDPOINT/$DOCKER_USER/platypush"
|
||||||
|
|
||||||
|
# Log in to the registry
|
||||||
|
docker login "$REGISTRY_ENDPOINT" -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
||||||
|
|
||||||
|
# Required for multi-platform builds
|
||||||
|
docker buildx create --name=multiarch --driver=docker-container
|
||||||
|
|
||||||
|
# Build and publish the images
|
||||||
|
docker buildx build \
|
||||||
|
-f Dockerfile.alpine \
|
||||||
|
-t "$IMAGE_NAME:$VERSION" \
|
||||||
|
-t "$IMAGE_NAME:latest" \
|
||||||
|
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||||
|
--builder multiarch \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
docker buildx rm multiarch
|
5
.drone/update-pip-package.sh
Executable file
5
.drone/update-pip-package.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
apk add --update --no-cache py3-twine py3-setuptools py3-wheel py3-pip
|
||||||
|
python setup.py sdist bdist_wheel
|
||||||
|
twine upload dist/platypush-$(python setup.py --version).tar.gz
|
261
.drone/update-rpm-repo.sh
Executable file
261
.drone/update-rpm-repo.sh
Executable file
|
@ -0,0 +1,261 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
[ -f .skipci ] && exit 0
|
||||||
|
|
||||||
|
echo "-- Installing dependencies"
|
||||||
|
yum install -y \
|
||||||
|
createrepo \
|
||||||
|
git \
|
||||||
|
gpg \
|
||||||
|
python \
|
||||||
|
python-pip \
|
||||||
|
python-setuptools \
|
||||||
|
rpm-build \
|
||||||
|
rpm-sign \
|
||||||
|
systemd-rpm-macros \
|
||||||
|
wget \
|
||||||
|
yum-utils \
|
||||||
|
|
||||||
|
echo "-- Copying source directory"
|
||||||
|
mkdir -p "$WORKDIR"
|
||||||
|
export SRCDIR="$WORKDIR/src"
|
||||||
|
cp -r "$PWD" "$SRCDIR"
|
||||||
|
cd "$SRCDIR"
|
||||||
|
mkdir -p "$RPM_ROOT"
|
||||||
|
|
||||||
|
echo "--- Parsing metadata"
|
||||||
|
git config --global --add safe.directory $PWD
|
||||||
|
git pull --rebase origin master --tags
|
||||||
|
export VERSION=$(grep -e '^__version__' "${SRCDIR}/version.py" | sed -r -e 's/^__version__\s*=\s*"([^"]+)"$/\1/')
|
||||||
|
export RELNUM="$(git log --pretty=oneline HEAD...v$VERSION | wc -l)"
|
||||||
|
export SPECFILE="$WORKDIR/$PKG_NAME.spec"
|
||||||
|
export BUILD_DIR="$WORKDIR/build"
|
||||||
|
export TMP_RPM_ROOT="$WORKDIR/repo"
|
||||||
|
export SRC_URL="https://git.platypush.tech/platypush/platypush/archive/master.tar.gz"
|
||||||
|
|
||||||
|
echo "--- Creating git package spec"
|
||||||
|
|
||||||
|
cat <<EOF > $SPECFILE
|
||||||
|
Summary: Universal command executor and automation hub.
|
||||||
|
Name: $PKG_NAME-git
|
||||||
|
Version: $VERSION
|
||||||
|
Release: $RELNUM
|
||||||
|
URL: https://platypush.tech
|
||||||
|
Group: System
|
||||||
|
License: MIT
|
||||||
|
Packager: Fabio Manganiello <fabio@platypush.tech>
|
||||||
|
Source: $SRC_URL
|
||||||
|
Requires: $(cat platypush/install/requirements/fedora.txt | tr '\n' ' ')
|
||||||
|
Conflicts: $PKG_NAME
|
||||||
|
Prefix: %{_prefix}
|
||||||
|
BuildRoot: %{_tmppath}/%{name}-root
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
%{?sysusers_requires_compat}
|
||||||
|
|
||||||
|
%description
|
||||||
|
Universal command executor and automation hub.
|
||||||
|
|
||||||
|
%install
|
||||||
|
mkdir -p %{buildroot}/
|
||||||
|
cp -r "$BUILD_DIR"/* %{buildroot}/
|
||||||
|
install -p -Dm0644 "${BUILD_DIR}/usr/lib/sysusers.d/platypush.conf" %{buildroot}%{_sysusersdir}/platypush.conf
|
||||||
|
|
||||||
|
%pre
|
||||||
|
%sysusers_create_compat "${BUILD_DIR}/usr/lib/sysusers.d/platypush.conf"
|
||||||
|
|
||||||
|
%clean
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(750,platypush,platypush,750)
|
||||||
|
%dir /etc/platypush
|
||||||
|
/etc/platypush/*
|
||||||
|
/usr/bin/*
|
||||||
|
/usr/lib/python$(python3 --version | awk '{print $2}' | cut -d. -f 1,2)/site-packages/platypush
|
||||||
|
/usr/lib/python$(python3 --version | awk '{print $2}' | cut -d. -f 1,2)/site-packages/platypush-$VERSION.dist-info
|
||||||
|
/usr/lib/systemd/system/*
|
||||||
|
/usr/lib/systemd/user/*
|
||||||
|
%defattr(750,platypush,platypush,750)
|
||||||
|
%dir /var/lib/platypush
|
||||||
|
%{_sysusersdir}/platypush.conf
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* $(date +'%a %b %d %Y') admin <admin@platypush.tech>
|
||||||
|
- [Automatic] Release $VERSION-$RELNUM
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "--- Building git package"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
pip install --prefix="$BUILD_DIR/usr" --no-cache --no-deps .
|
||||||
|
|
||||||
|
install -m755 -d "${BUILD_DIR}/usr/lib/systemd/system"
|
||||||
|
install -m755 -d "${BUILD_DIR}/usr/lib/systemd/user"
|
||||||
|
install -m755 -d "${BUILD_DIR}/usr/lib/sysusers.d"
|
||||||
|
install -m750 -d "${BUILD_DIR}/var/lib/platypush"
|
||||||
|
install -m750 -d "${BUILD_DIR}/etc/platypush/scripts"
|
||||||
|
|
||||||
|
install -m644 "${SRCDIR}/platypush/config/config.yaml" "${BUILD_DIR}/etc/platypush/config.yaml"
|
||||||
|
install -Dm644 "${SRCDIR}/platypush/config/systemd/platypush-sysusers.conf" "${BUILD_DIR}/usr/lib/sysusers.d/platypush.conf"
|
||||||
|
install -m644 "${SRCDIR}/platypush/config/systemd/platypush.service" "${BUILD_DIR}/usr/lib/systemd/user/platypush.service"
|
||||||
|
install -m644 "${SRCDIR}/platypush/config/systemd/platypush.service" "${BUILD_DIR}/usr/lib/systemd/system/platypush.service"
|
||||||
|
sed -i "${BUILD_DIR}/usr/lib/systemd/system/platypush.service" -r \
|
||||||
|
-e 's/^#\s*Requires=(.*)/Requires=\1/' \
|
||||||
|
-e 's/^\[Service\]$/\[Service\]\
|
||||||
|
User=platypush\
|
||||||
|
Group=platypush\
|
||||||
|
WorkingDirectory=\/var\/lib\/platypush\
|
||||||
|
Environment="PLATYPUSH_CONFIG=\/etc\/platypush\/config.yaml"\
|
||||||
|
Environment="PLATYPUSH_WORKDIR=\/var\/lib\/platypush"/'
|
||||||
|
|
||||||
|
rpmbuild --target "noarch" -bb "$SPECFILE"
|
||||||
|
|
||||||
|
echo "--- Copying the new RPM package"
|
||||||
|
mkdir -p "$TMP_RPM_ROOT"
|
||||||
|
cp "$HOME/rpmbuild/RPMS/noarch/$PKG_NAME-git-$VERSION-$RELNUM.noarch.rpm" "$TMP_RPM_ROOT"
|
||||||
|
|
||||||
|
echo "--- Checking the latest released stable version"
|
||||||
|
export LATEST_STABLE_PKG=$(ls -rt "$RPM_ROOT/$PKG_NAME"*.rpm 2>/dev/null | grep -v "$PKG_NAME-git" | tail -1)
|
||||||
|
|
||||||
|
if [ -z "$LATEST_STABLE_PKG" ]; then
|
||||||
|
# If not stable release is available, then create one
|
||||||
|
export UPDATE_STABLE_PKG=1
|
||||||
|
else
|
||||||
|
# Otherwise, create a new release if the reported version on the repo is different
|
||||||
|
# from the latest released version.
|
||||||
|
export LATEST_STABLE_VERSION=$(basename $LATEST_STABLE_PKG | cut -d- -f 2)
|
||||||
|
if [ "$VERSION" != "$LATEST_STABLE_VERSION" ]; then
|
||||||
|
export UPDATE_STABLE_PKG=1
|
||||||
|
else
|
||||||
|
# If the version has remained the same, then simply copy the existing RPM to the
|
||||||
|
# new repository directory.
|
||||||
|
echo "Copying the existing release $LATEST_STABLE_VERSION to the new repository"
|
||||||
|
cp "$LATEST_STABLE_PKG" "$TMP_RPM_ROOT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If a new stable release is required, build another RPM
|
||||||
|
if [ -n "$UPDATE_STABLE_PKG" ]; then
|
||||||
|
export RELNUM=1
|
||||||
|
export SRC_URL="https://git.platypush.tech/platypush/platypush/archive/v$VERSION.tar.gz"
|
||||||
|
|
||||||
|
cat <<EOF > $SPECFILE
|
||||||
|
Summary: Universal command executor and automation hub.
|
||||||
|
Name: $PKG_NAME
|
||||||
|
Version: $VERSION
|
||||||
|
Release: $RELNUM
|
||||||
|
URL: https://platypush.tech
|
||||||
|
Group: System
|
||||||
|
License: MIT
|
||||||
|
Packager: Fabio Manganiello <fabio@platypush.tech>
|
||||||
|
Source: $SRC_URL
|
||||||
|
Requires: $(cat platypush/install/requirements/fedora.txt | tr '\n' ' ')
|
||||||
|
Conflicts: $PKG_NAME-git
|
||||||
|
Prefix: %{_prefix}
|
||||||
|
BuildRoot: %{_tmppath}/%{name}-root
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
%{?sysusers_requires_compat}
|
||||||
|
|
||||||
|
%description
|
||||||
|
Universal command executor and automation hub.
|
||||||
|
|
||||||
|
%install
|
||||||
|
mkdir -p %{buildroot}/
|
||||||
|
cp -r "$BUILD_DIR"/* %{buildroot}/
|
||||||
|
install -p -Dm0644 "${BUILD_DIR}/usr/lib/sysusers.d/platypush.conf" %{buildroot}%{_sysusersdir}/platypush.conf
|
||||||
|
|
||||||
|
%pre
|
||||||
|
%sysusers_create_compat "${BUILD_DIR}/usr/lib/sysusers.d/platypush.conf"
|
||||||
|
|
||||||
|
%clean
|
||||||
|
|
||||||
|
%files
|
||||||
|
%defattr(750,platypush,platypush,750)
|
||||||
|
%dir /etc/platypush
|
||||||
|
/etc/platypush/*
|
||||||
|
/usr/bin/*
|
||||||
|
/usr/lib/python$(python3 --version | awk '{print $2}' | cut -d. -f 1,2)/site-packages/platypush
|
||||||
|
/usr/lib/python$(python3 --version | awk '{print $2}' | cut -d. -f 1,2)/site-packages/platypush-$VERSION.dist-info
|
||||||
|
/usr/lib/systemd/system/*
|
||||||
|
/usr/lib/systemd/user/*
|
||||||
|
%defattr(750,platypush,platypush,750)
|
||||||
|
%dir /var/lib/platypush
|
||||||
|
%{_sysusersdir}/platypush.conf
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* $(date +'%a %b %d %Y') admin <admin@platypush.tech>
|
||||||
|
- [Automatic] Release $VERSION-$RELNUM
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "--- Building package for stable release $VERSION"
|
||||||
|
rpmbuild --target "noarch" -bb "$SPECFILE"
|
||||||
|
cp "$HOME/rpmbuild/RPMS/noarch/$PKG_NAME-$VERSION-$RELNUM.noarch.rpm" "$TMP_RPM_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- Importing the repository keys"
|
||||||
|
cat <<EOF | gpg --import --armor
|
||||||
|
$PGP_PRIVKEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
export PGP_KEYID=$(echo "$PGP_PUBKEY" | gpg --with-colons --import-options show-only --import --fingerprint | grep -e '^fpr:' | head -1 | awk -F ':' '{print $(NF - 1)}')
|
||||||
|
cat <<EOF > $HOME/.rpmmacros
|
||||||
|
%signature gpg
|
||||||
|
%_gpg_name $PGP_KEYID
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "--- Signing the new RPM packages"
|
||||||
|
rpm --addsign "$TMP_RPM_ROOT"/*.rpm
|
||||||
|
|
||||||
|
echo "--- Creating a new copy of the RPM repository"
|
||||||
|
createrepo "$TMP_RPM_ROOT"
|
||||||
|
gpg --detach-sign --armor "$TMP_RPM_ROOT/repodata/repomd.xml"
|
||||||
|
|
||||||
|
cat <<EOF > "$TMP_RPM_ROOT/platypush.repo"
|
||||||
|
[platypush]
|
||||||
|
name=Platypush repository
|
||||||
|
baseurl=https://rpm.platypush.tech
|
||||||
|
enabled=1
|
||||||
|
type=rpm
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://rpm.platypush.tech/pubkey.txt
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > "$TMP_RPM_ROOT/index.txt"
|
||||||
|
Welcome to the Platypush RPM repository!
|
||||||
|
|
||||||
|
Project homepage: https://platypush.tech
|
||||||
|
Source code: https://git.platypush.tech/platypush/platypush
|
||||||
|
Documentation / API reference: https://docs.platypush.tech
|
||||||
|
|
||||||
|
You can use this RPM repository to install Platypush on Fedora or other
|
||||||
|
RPM-based distros - as long as they are compatible with the latest Fedora
|
||||||
|
release.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
1. Add the repository to your sources
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
# yum config-manager --add-repo https://rpm.platypush.tech/platypush.repo
|
||||||
|
|
||||||
|
2. Install Platypush
|
||||||
|
====================
|
||||||
|
|
||||||
|
# yum install platypush
|
||||||
|
|
||||||
|
Or, if you want to install a version always up-to-date with the git repo:
|
||||||
|
|
||||||
|
# yum install platypush-git
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > "$TMP_RPM_ROOT/pubkey.txt"
|
||||||
|
$PGP_PUBKEY
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "--- Updating the repository"
|
||||||
|
export NEW_RPM_ROOT="$REPOS_ROOT/rpm_new"
|
||||||
|
export OLD_RPM_ROOT="$REPOS_ROOT/rpm_old"
|
||||||
|
cp -r "$TMP_RPM_ROOT" "$NEW_RPM_ROOT"
|
||||||
|
rm -rf "$TMP_RPM_ROOT"
|
||||||
|
mv "$RPM_ROOT" "$OLD_RPM_ROOT"
|
||||||
|
mv "$NEW_RPM_ROOT" "$RPM_ROOT"
|
||||||
|
rm -rf "$OLD_RPM_ROOT"
|
22
.env.example
Normal file
22
.env.example
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# The device ID is the unique identifier for the device that runs Platypush.
|
||||||
|
# You should make sure that it's unique at least within your local network,
|
||||||
|
# as it is used to identify the device in the MQTT topics, on the HTTP API
|
||||||
|
# and on the published ZeroConf services.
|
||||||
|
PLATYPUSH_DEVICE_ID=platypush
|
||||||
|
|
||||||
|
# Use an external Redis server for the message queue. By default, the Platypush
|
||||||
|
# container will run a Redis server on the same container. Also remove the
|
||||||
|
# `--start-redis` option from the `docker run` command if you want to use an
|
||||||
|
# external Redis server.
|
||||||
|
# PLATYPUSH_REDIS_HOST=localhost
|
||||||
|
# PLATYPUSH_REDIS_PORT=6379
|
||||||
|
|
||||||
|
# Custom location for the Platypush configuration file.
|
||||||
|
# PLATYPUSH_CONFIG=/etc/platypush/config.yaml
|
||||||
|
|
||||||
|
# Custom location for the Platypush working directory.
|
||||||
|
# PLATYPUSH_WORKDIR=/var/lib/platypush
|
||||||
|
|
||||||
|
# SQLAlchemy database URL. By default, the Platypush container will run on a
|
||||||
|
# SQLite database installed under <WORKDIR>/main.db. If you want
|
||||||
|
# PLATYPUSH_DB=sqlite:////var/lib/platypush/main.db
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,3 +28,4 @@ Session.vim
|
||||||
/docs/source/wiki
|
/docs/source/wiki
|
||||||
/.skipci
|
/.skipci
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
.env
|
||||||
|
|
1
.ignore
Normal file
1
.ignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
dist/
|
|
@ -2,7 +2,7 @@
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.1.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
# - id: trailing-whitespace
|
# - id: trailing-whitespace
|
||||||
# - id: end-of-file-fixer
|
# - id: end-of-file-fixer
|
||||||
|
@ -11,15 +11,15 @@ repos:
|
||||||
- id: check-xml
|
- id: check-xml
|
||||||
- id: check-symlinks
|
- id: check-symlinks
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
args: ['--maxkb=1500']
|
args: ['--maxkb=3000']
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
|
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
|
||||||
rev: v1.1.2
|
rev: v1.1.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: markdown-toc
|
- id: markdown-toc
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 4.0.1
|
rev: 7.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
|
356
CHANGELOG.md
356
CHANGELOG.md
|
@ -1,8 +1,358 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
## [1.3.4]
|
||||||
Given the high speed of development in the first phase, changes are being
|
|
||||||
reported only starting from v0.20.2.
|
- [Bug]: Fixed installation bug in `pip install platypush` introduced by the
|
||||||
|
`pyproject.toml` migration.
|
||||||
|
|
||||||
|
## [1.3.3]
|
||||||
|
|
||||||
|
- [`3e02304a`](https://git.platypush.tech/platypush/platypush/commit/3e02304ac203625650ab4b03f9d4146a40839f2f)
|
||||||
|
[Auth]: Fixed generation of API tokens when 2FA is enabled.
|
||||||
|
|
||||||
|
## [1.3.2]
|
||||||
|
|
||||||
|
- [[#414](https://git.platypush.tech/platypush/platypush/issues/414)]: added
|
||||||
|
Jellyfin UI for all media plugins. This makes it possible to browse and play
|
||||||
|
media items from the Jellyfin library on multiple devices.
|
||||||
|
|
||||||
|
- [[#434](https://git.platypush.tech/platypush/platypush/issues/434)]: added
|
||||||
|
official Docker base image - [see
|
||||||
|
documentation](https://docs.platypush.tech/wiki/Installation.html#docker).
|
||||||
|
|
||||||
|
- [_Other Docker improvements_]
|
||||||
|
* Reduced size for the base images.
|
||||||
|
* Better self-documented [`docker-compose.yml`
|
||||||
|
file](https://git.platypush.tech/platypush/platypush/src/branch/master/docker-compose.yml).
|
||||||
|
|
||||||
|
- [[`db`](https://git.platypush.tech/platypush/platypush/commit/dd02be12)]:
|
||||||
|
Fixed compatibility of transaction scopes with SQLAlchemy 2.0.
|
||||||
|
|
||||||
|
- [`media.vlc`]: Improved state management.
|
||||||
|
|
||||||
|
- [`media.mpv`]: Enhanced compatibility. mpv 1.0 introduced many breaking
|
||||||
|
changes in the event management API, but it may not yet be available on all
|
||||||
|
distros. Platypush has now introduced a compatibility layer to seamlessly
|
||||||
|
interact with any version of mpv.
|
||||||
|
|
||||||
|
- [_Media UI_] Added support for embedded players for Jellyfin, YouTube and
|
||||||
|
local media.
|
||||||
|
|
||||||
|
- [[`http.webpage`](https://git.platypush.tech/platypush/platypush/commit/09413bc0)]:
|
||||||
|
Added support for custom headers to the scraping functions.
|
||||||
|
|
||||||
|
## [1.3.1]
|
||||||
|
|
||||||
|
- [[#344](https://git.platypush.tech/platypush/platypush/issues/344)]: removed
|
||||||
|
`marshmallow_dataclass` dependency. That package isn't included in the
|
||||||
|
package managers of any supported distros and requires to be installed via
|
||||||
|
pip. Making the Platypush' system packages depend on a pip-only package is
|
||||||
|
not a good idea. Plus, the library seems to be still in heavy development and
|
||||||
|
it has already broken compatibility with at least the `system` package.
|
||||||
|
|
||||||
|
## [1.3.0]
|
||||||
|
|
||||||
|
- [[#333](https://git.platypush.tech/platypush/platypush/issues/333)]: new file
|
||||||
|
browser UI/component. It includes custom MIME type support, a file editor
|
||||||
|
with syntax highlight, file download and file upload.
|
||||||
|
|
||||||
|
- [[#341](https://git.platypush.tech/platypush/platypush/issues/341)]:
|
||||||
|
procedures are now native entities that can be managed from the entities panel.
|
||||||
|
A new versatile procedure editor has also been added, with support for nested
|
||||||
|
blocks, conditions, loops, variables, context autocomplete, and more.
|
||||||
|
|
||||||
|
- [`procedure`]: Added the following features to YAML/structured procedures:
|
||||||
|
|
||||||
|
- `set`: to set variables whose scope is limited to the procedure / code
|
||||||
|
block where they are created. `variable.set` is useful to permanently
|
||||||
|
store variables on the db, `variable.mset` is useful to set temporary
|
||||||
|
global variables in memory through Redis, but sometimes you may just want
|
||||||
|
to assign a value to a variable that only needs to live within a procedure,
|
||||||
|
event hook or cron.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- set:
|
||||||
|
foo: bar
|
||||||
|
temperature: ${output.get('temperature')}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `return` can now return values too when invoked within a procedure:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- return: something
|
||||||
|
# Or
|
||||||
|
- return: "Result: ${output.get('response')}"
|
||||||
|
```
|
||||||
|
|
||||||
|
- The default logging format is now much more compact. The full body of events
|
||||||
|
and requests is no longer included by default in `info` mode - instead, a
|
||||||
|
summary with the message type, ID and response time is logged. The full
|
||||||
|
payloads can still be logged by enabling `debug` logs through e.g. `-v`.
|
||||||
|
|
||||||
|
## [1.2.3]
|
||||||
|
|
||||||
|
- [[#422](https://git.platypush.tech/platypush/platypush/issues/422)]: adapted
|
||||||
|
media plugins to support streaming from the yt-dlp process. This allows
|
||||||
|
videos to have merged audio+video even if they had separate tracks upstream.
|
||||||
|
|
||||||
|
- [`media.*`] Many improvements on the media UI.
|
||||||
|
|
||||||
|
- [`zigbee.mqtt`] Removed synchronous logic from `zigbee.mqtt.device_set`. It
|
||||||
|
was prone to timeouts as well as pointless - the updated device state will
|
||||||
|
anyway be received as an event.
|
||||||
|
|
||||||
|
## [1.2.2]
|
||||||
|
|
||||||
|
- Fixed regression on older version of Python that don't fully support
|
||||||
|
`pyproject.toml` and can't install data files the new way.
|
||||||
|
|
||||||
|
## [1.2.1]
|
||||||
|
|
||||||
|
- Added static `/login` and `/register` Flask fallback routes to prevent 404 if
|
||||||
|
the client doesn't have JavaScript enabled.
|
||||||
|
|
||||||
|
- Fixed `apt` packages for Debian oldstable after the `setup.py` to
|
||||||
|
`pyproject.toml` migration.
|
||||||
|
|
||||||
|
- Fixed license string in the `pyproject.toml`.
|
||||||
|
|
||||||
|
## [1.2.0]
|
||||||
|
|
||||||
|
- [#419](https://git.platypush.tech/platypush/platypush/issues/419): added
|
||||||
|
support for randomly generated API tokens alongside JWT.
|
||||||
|
|
||||||
|
- [#339](https://git.platypush.tech/platypush/platypush/issues/339): added
|
||||||
|
support for 2FA with OTP codes.
|
||||||
|
|
||||||
|
- [#393](https://git.platypush.tech/platypush/platypush/issues/393): added
|
||||||
|
`bind_socket` parameter to `backend.http`, so Platypush can listen on (or
|
||||||
|
exclusively if `listen_port` is null) on a local UNIX socket as well.
|
||||||
|
|
||||||
|
- [#401](https://git.platypush.tech/platypush/platypush/issues/401): added
|
||||||
|
`--redis-bin` option / `PLATYPUSH_REDIS_BIN` environment variable to support
|
||||||
|
custom Redis (or drop-in replacements) executables when `--start-redis` is
|
||||||
|
specified.
|
||||||
|
|
||||||
|
- [#413](https://git.platypush.tech/platypush/platypush/issues/401): added
|
||||||
|
support for page-specific PWAs. If you navigate to `/plugin/<plugin-name>`,
|
||||||
|
and you install it as a PWA, you'll install a PWA only for that plugin - not
|
||||||
|
for the whole Platypush UI.
|
||||||
|
|
||||||
|
- Migrated project setup from `setup.py` to `pyproject.toml`.
|
||||||
|
|
||||||
|
- [`70db33b4e`](https://git.platypush.tech/platypush/platypush/commit/70db33b4e):
|
||||||
|
more application resilience in case Redis goes down.
|
||||||
|
|
||||||
|
- [`ee27b2c4`](https://git.platypush.tech/platypush/platypush/commit/ee27b2c4):
|
||||||
|
Refactor of all the authentication endpoints into a single `/auth` endpoint:
|
||||||
|
|
||||||
|
- `POST /register` → `POST /auth?type=register`
|
||||||
|
- `POST /login` → `POST /auth?type=login`
|
||||||
|
- `POST /auth` → `POST /auth?type=token`
|
||||||
|
- `POST /auth` → `POST /auth?type=jwt`
|
||||||
|
|
||||||
|
- [`2ccf0050`](https://git.platypush.tech/platypush/platypush/commit/2ccf0050):
|
||||||
|
Added support for binary content to `qrcode.generate`.
|
||||||
|
|
||||||
|
- [`b69e9500`](https://git.platypush.tech/platypush/platypush/commit/b69e9500):
|
||||||
|
Support for fullscreen mode on the `camera` plugins UI.
|
||||||
|
|
||||||
|
## [1.1.3] - 2024-07-16
|
||||||
|
|
||||||
|
- [`core`]: New architecture for the Redis bus - now leveraging pub/sub with a
|
||||||
|
connection pool instead of a single-connection queue. It makes the
|
||||||
|
application much faster and less prone to Redis deadlocks.
|
||||||
|
|
||||||
|
- [`youtube`]:
|
||||||
|
[#391](https://git.platypush.tech/platypush/platypush/issues/391): added
|
||||||
|
support for:
|
||||||
|
|
||||||
|
- Add/remove playlists (UI)
|
||||||
|
- Add to/remove from playlist (UI)
|
||||||
|
- Subscribe/unsubscribe from channels (UI)
|
||||||
|
- Browse channels and playlists directly in the UI
|
||||||
|
- Download media and audio
|
||||||
|
|
||||||
|
## [1.1.1] - 2024-06-24
|
||||||
|
|
||||||
|
- [`torrent`]: [[#263](https://git.platypush.tech/platypush/platypush/issues/263)], [[#375](https://git.platypush.tech/platypush/platypush/issues/375)],
|
||||||
|
[[#407](https://git.platypush.tech/platypush/platypush/issues/407)] and
|
||||||
|
[[#408](https://git.platypush.tech/platypush/platypush/issues/408)]: added
|
||||||
|
torrents.csv search provider and rewritten torrents UI.
|
||||||
|
|
||||||
|
- [[#281](https://git.platypush.tech/platypush/platypush/issues/281)]
|
||||||
|
replaced `warnings.warn` with `logging.warning`, as there is no easy and
|
||||||
|
reliable way of routing `warnings.warn` to `logging`.
|
||||||
|
|
||||||
|
## [1.1.0] - 2024-06-06
|
||||||
|
|
||||||
|
- [[#405](https://git.platypush.tech/platypush/platypush/issues/405)] Fixed
|
||||||
|
timezone/timestamp rendering issues for `calendar.ical` events.
|
||||||
|
- [[#403](https://git.platypush.tech/platypush/platypush/issues/403)]
|
||||||
|
Included inherited actions in plugins docs.
|
||||||
|
|
||||||
|
## [1.0.7] - 2024-06-02
|
||||||
|
|
||||||
|
- [[#384](https://git.platypush.tech/platypush/platypush/issues/384)] Added
|
||||||
|
`assistant.openai` and `tts.openai` plugins.
|
||||||
|
|
||||||
|
## [1.0.6] - 2024-06-01
|
||||||
|
|
||||||
|
- 🐛 Bug fix on one of the entities modules that prevented the application from
|
||||||
|
loading when `.` is part of `PYTHONPATH`.
|
||||||
|
|
||||||
|
## [1.0.5] - 2024-06-01
|
||||||
|
|
||||||
|
- A proper solution for the `utcnow()` issue.
|
||||||
|
|
||||||
|
It was a bit trickier than expected to solve, but now Platypush uses a
|
||||||
|
`utcnow()` facade that always returns a UTC datetime in a timezone-aware
|
||||||
|
representation.
|
||||||
|
|
||||||
|
The code should however also handle the case of timestamps stored on the db in
|
||||||
|
the old format.
|
||||||
|
|
||||||
|
## [1.0.4] - 2024-05-31
|
||||||
|
|
||||||
|
- Fixed regression introduced by
|
||||||
|
[c18768e61fef62924f4c1fac3089ecfb83666dab](https://git.platypush.tech/platypush/platypush/commit/c18768e61fef62924f4c1fac3089ecfb83666dab).
|
||||||
|
Python seems to have introduced a breaking change from the version 3.12 -
|
||||||
|
`datetime.utcnow()` is not deprecated, but `datetime.UTC`, the suggested
|
||||||
|
alternative, isn't available on older versions of Python. Added a workaround
|
||||||
|
that makes Platypush compatible with both the implementations.
|
||||||
|
|
||||||
|
## [1.0.3] - 2024-05-31
|
||||||
|
|
||||||
|
- [[#368](https://git.platypush.tech/platypush/platypush/issues/368)] Added
|
||||||
|
Ubuntu packages.
|
||||||
|
|
||||||
|
- Fixed bug that didn't get hooks to match events imported through the new
|
||||||
|
`platypush.events` symlinked module.
|
||||||
|
|
||||||
|
## [1.0.2] - 2024-05-26
|
||||||
|
|
||||||
|
- Fixed regression introduced by the support of custom names through the
|
||||||
|
`@procedure` decorator.
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-05-26
|
||||||
|
|
||||||
|
Many, many changes for the first major release of Platypush after so many
|
||||||
|
years.
|
||||||
|
|
||||||
|
- [!3](https://git.platypush.tech/platypush/platypush/milestone/3) All
|
||||||
|
backends, except for `http`, `nodered`, `redis` and `tcp`, are gone. Many
|
||||||
|
were already deprecated a while ago, but the change now applies to all of
|
||||||
|
them. Backends should only be components that actively listen for application
|
||||||
|
messages to process, not generic daemon threads for integrations. This had
|
||||||
|
been a source of confusion for a long time. Backends and plugins are now
|
||||||
|
merged, meaning that you won't need to configure two different sections
|
||||||
|
instead of one for many integrations (one for the stateless plugin, and one
|
||||||
|
for the background state listener). Please check the
|
||||||
|
[documentation](https://docs.platypush.tech) to verify the configuration
|
||||||
|
changes required by your integrations. This has been a long process that has
|
||||||
|
involved the rewrite of most of the integrations, and many bugs have been
|
||||||
|
fixed.
|
||||||
|
|
||||||
|
- Improved Docker support - now with a default `docker-compose.yml`, multiple
|
||||||
|
Dockerfiles for
|
||||||
|
[Alpine](https://git.platypush.tech/platypush/platypush/src/branch/master/platypush/install/docker/alpine.Dockerfile),
|
||||||
|
[Debian](https://git.platypush.tech/platypush/platypush/src/branch/master/platypush/install/docker/debian.Dockerfile),
|
||||||
|
[Ubuntu](https://git.platypush.tech/platypush/platypush/src/branch/master/platypush/install/docker/ubuntu.Dockerfile)
|
||||||
|
and
|
||||||
|
[Fedora](https://git.platypush.tech/platypush/platypush/src/branch/master/platypush/install/docker/fedora.Dockerfile)
|
||||||
|
base images. Many improvements on the `platydock` and `platyvenv` scripts
|
||||||
|
too, with better automated installation processes for optional dependencies.
|
||||||
|
|
||||||
|
- Added [official
|
||||||
|
packages](https://git.platypush.tech/platypush/platypush#system-package-manager-installation)
|
||||||
|
for
|
||||||
|
[Debian](https://git.platypush.tech/platypush/platypush#debian-ubuntu)
|
||||||
|
and [Fedora](https://git.platypush.tech/platypush/platypush#fedora).
|
||||||
|
|
||||||
|
- Added `--device-id`, `--workdir`, `--logsdir`, `--cachedir`, `--main-db`,
|
||||||
|
`--redis-host`, `--redis-port` and `--redis-queue` CLI arguments, along the
|
||||||
|
`PLATYPUSH_DEVICE_ID`, `PLATYPUSH_WORKDIR`, `PLATYPUSH_LOGSDIR`,
|
||||||
|
`PLATYPUSH_CACHEDIR`, `PLATYPUSH_DB`, `PLATYPUSH_REDIS_HOST`,
|
||||||
|
`PLATYPUSH_REDIS_PORT` and `PLATYPUSH_REDIS_QUEUE` environment variables.
|
||||||
|
|
||||||
|
- Added an _Extensions_ panel to the UI to dynamically:
|
||||||
|
- Install new dependencies directly from the Web view.
|
||||||
|
- Explore the documentation as well as the supported actions and events for
|
||||||
|
each plugin.
|
||||||
|
- Get ready-to-paste configuration snippets/templates.
|
||||||
|
|
||||||
|
- New, completely rewritten [documentation](https://docs.platypush.tech), which
|
||||||
|
now integrates the wiki, dynamically includes plugins configuration snippets
|
||||||
|
and dependencies, and adds a global filter bar for the integrations.
|
||||||
|
|
||||||
|
- [[#394](https://git.platypush.tech/platypush/platypush/issues/394)] A more
|
||||||
|
intuitive way of installing extra dependencies via `pip`. Instead of a static
|
||||||
|
list that the user should check inside of `setup.py`, the syntax `pip install
|
||||||
|
'platypush[plugin1,plugin2,...]'` is now supported.
|
||||||
|
|
||||||
|
- No more need to manually create `__init__.py` in each of the `scripts`
|
||||||
|
folders that you want to use to store your custom scripts. Automatic
|
||||||
|
discovery of scripts and creation of module files has been implemented. You
|
||||||
|
can now just drop a `.py` script with your procedures, hooks or crons in the
|
||||||
|
scripts folder and it should be picked up by the application.
|
||||||
|
|
||||||
|
- The _Execute_ Web panel now supports procedures too, as well as curl snippets.
|
||||||
|
|
||||||
|
- Removed all `Response` objects outside of the root type. They are now all
|
||||||
|
replaced by Marshmallow schemas with the structure automatically generated in
|
||||||
|
the documentation.
|
||||||
|
|
||||||
|
- [`alarm`] [[#340](https://git.platypush.tech/platypush/platypush/issues/340)]
|
||||||
|
Rewritten integration. It now includes a powerful UI panel to set up alarms
|
||||||
|
with custom procedures too.
|
||||||
|
|
||||||
|
- [`assistant.picovoice`]
|
||||||
|
[[#304](https://git.platypush.tech/platypush/platypush/issues/304)] New
|
||||||
|
all-in-one Picovoice integration that replaces the previous `stt.picovoice.*`
|
||||||
|
integrations.
|
||||||
|
|
||||||
|
- [`youtube`]
|
||||||
|
[[#337](https://git.platypush.tech/platypush/platypush/issues/337)] Full
|
||||||
|
rewrite of the plugin. It now supports Piped instances instead of the
|
||||||
|
official YouTube API. A new UI has also been designed to explore
|
||||||
|
subscriptions, playlists and channels.
|
||||||
|
|
||||||
|
- [`weather.*`]
|
||||||
|
[[#308](https://git.platypush.tech/platypush/platypush/issues/308)] Removed
|
||||||
|
the `weather.darksky` integration (it's now owned by Apple and the API is
|
||||||
|
basically dead) and enhanced the `weather.openweathermap` plugin instead.
|
||||||
|
|
||||||
|
- [`camera.pi*`] The old `camera.pi` integration based on the deprecated
|
||||||
|
`picamera` module has been moved to `camera.pi.legacy`. `camera.pi` is now a
|
||||||
|
new plugin which uses the new `picamera2` API (and it's so far only
|
||||||
|
compatible with recent versions on the Raspberry Pi OS).
|
||||||
|
|
||||||
|
- Dynamically auto-generate plugins documentation in the UI from the RST
|
||||||
|
docstrings.
|
||||||
|
|
||||||
|
- New design for the configuration panel.
|
||||||
|
|
||||||
|
- Better synchronization between processes on threads on application stop -
|
||||||
|
greatly reduced the risk of hanging processes on shutdown.
|
||||||
|
|
||||||
|
- Migrated all CI/CD pipelines to [Drone
|
||||||
|
CI](https://ci-cd.platypush.tech/platypush/platypush).
|
||||||
|
|
||||||
|
- Removed `google.fit` integration, as Google has deprecated the API.
|
||||||
|
|
||||||
|
- Removed `wiimote` integration: the `cwiid` library hasn't been updated in
|
||||||
|
years, it doesn't even work well with Python 3, and I'm not in the mood of
|
||||||
|
bringing it back from the dead.
|
||||||
|
|
||||||
|
- Removed `stt.deepspeech` integration. That project has been basically
|
||||||
|
abandoned by Mozilla, the libraries are very buggy and I don't think it's
|
||||||
|
going to see new developments any time soon.
|
||||||
|
|
||||||
|
- [[#297](https://git.platypush.tech/platypush/platypush/issues/297)] Removed
|
||||||
|
`spotify` backend integration based on Librespot. The project has gone
|
||||||
|
through a lot of changes, and I no longer have a Spotify premium account to
|
||||||
|
work on a new implementation. Open to contributions if somebody still wants
|
||||||
|
it.
|
||||||
|
|
||||||
## [0.50.3] - 2023-07-22
|
## [0.50.3] - 2023-07-22
|
||||||
|
|
||||||
|
|
|
@ -27,13 +27,9 @@ Guidelines:
|
||||||
you are changing some of the core entities (e.g. requests, events, procedures, hooks, crons
|
you are changing some of the core entities (e.g. requests, events, procedures, hooks, crons
|
||||||
or the bus) then make sure to add tests and not to break the existing tests.
|
or the bus) then make sure to add tests and not to break the existing tests.
|
||||||
|
|
||||||
- If the feature requires an optional dependency then make sure to document it:
|
- If the feature requires an optional dependency then make sure to document it
|
||||||
|
in the `manifest.json` - refer to the Wiki (how to write
|
||||||
- In the class docstring (see other plugins and backends for examples).
|
|
||||||
- In [`setup.py`](https://git.platypush.tech/platypush/platypush/-/blob/master/setup.py#L72) as
|
|
||||||
an `extras_require` entry.
|
|
||||||
- In the plugin/backend class pydoc string.
|
|
||||||
- In the `manifest.yaml` - refer to the Wiki (how to write
|
|
||||||
[plugins](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-plugins)
|
[plugins](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-plugins)
|
||||||
and [backends](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-backends))
|
and
|
||||||
|
[backends](https://git.platypush.tech/platypush/platypush/wiki/Writing-your-own-backends))
|
||||||
for examples on how to write an extension manifest file.
|
for examples on how to write an extension manifest file.
|
||||||
|
|
1
Dockerfile.alpine
Symbolic link
1
Dockerfile.alpine
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
platypush/install/docker/alpine.Dockerfile
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2017, 2020 Fabio Manganiello
|
Copyright (c) 2017, 2024 Fabio Manganiello
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
13
MANIFEST.in
13
MANIFEST.in
|
@ -2,5 +2,16 @@ recursive-include platypush/backend/http/webapp/dist *
|
||||||
recursive-include platypush/install *
|
recursive-include platypush/install *
|
||||||
include platypush/plugins/http/webpage/mercury-parser.js
|
include platypush/plugins/http/webpage/mercury-parser.js
|
||||||
include platypush/config/*.yaml
|
include platypush/config/*.yaml
|
||||||
global-include manifest.yaml
|
include platypush/config/systemd/*
|
||||||
|
global-include manifest.json
|
||||||
global-include components.json.gz
|
global-include components.json.gz
|
||||||
|
|
||||||
|
global-exclude __pycache__/*
|
||||||
|
global-exclude *.pyc
|
||||||
|
|
||||||
|
# Exclude symlinks to avoid issues with setuptools
|
||||||
|
exclude platypush/backend/http/webapp/public/icons/openweathermap/black
|
||||||
|
exclude platypush/backend/http/webapp/public/icons/openweathermap/white
|
||||||
|
exclude platypush/backend/http/webapp/src/**
|
||||||
|
exclude platypush/events
|
||||||
|
exclude platypush/install/scripts/ubuntu
|
||||||
|
|
61
docker-compose.yml
Normal file
61
docker-compose.yml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
services:
|
||||||
|
platypush:
|
||||||
|
# Replace the build section with the next line if instead of building the
|
||||||
|
# image from a local checkout you want to pull the latest base
|
||||||
|
# (Alpine-based) image from the remote registry
|
||||||
|
# image: "quay.io/platypush/platypush:latest"
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
# Alpine base image
|
||||||
|
dockerfile: ./platypush/install/docker/alpine.Dockerfile
|
||||||
|
# Debian base image
|
||||||
|
# dockerfile: ./platypush/install/docker/debian.Dockerfile
|
||||||
|
# Ubuntu base image
|
||||||
|
# dockerfile: ./platypush/install/docker/ubuntu.Dockerfile
|
||||||
|
# Fedora base image
|
||||||
|
# dockerfile: ./platypush/install/docker/fedora.Dockerfile
|
||||||
|
|
||||||
|
restart: "always"
|
||||||
|
command:
|
||||||
|
- platypush
|
||||||
|
- --redis-host
|
||||||
|
- redis
|
||||||
|
# Or, if you want to run Redis from the same container as Platypush,
|
||||||
|
# replace --redis-host redis with the line below
|
||||||
|
# - --start-redis
|
||||||
|
|
||||||
|
# Custom list of host devices that should be accessible to the container -
|
||||||
|
# e.g. an Arduino, an ESP-compatible microcontroller, a joystick etc.
|
||||||
|
# devices:
|
||||||
|
# - /dev/ttyUSB0
|
||||||
|
|
||||||
|
# Uncomment if you need plugins that require access to low-level hardware
|
||||||
|
# (e.g. Bluetooth BLE or GPIO/SPI/I2C) if access to individual devices is
|
||||||
|
# not enough or isn't practical
|
||||||
|
# privileged: true
|
||||||
|
|
||||||
|
# Copy .env.example to .env and modify as needed
|
||||||
|
# env_file:
|
||||||
|
# - .env
|
||||||
|
|
||||||
|
ports:
|
||||||
|
# Comment if you don't have the HTTP backend enable or you don't want to
|
||||||
|
# expose it
|
||||||
|
- "8008:8008"
|
||||||
|
|
||||||
|
# volumes:
|
||||||
|
# Replace with a path that contains/will contain your config.yaml file
|
||||||
|
# - /path/to/your/config:/etc/platypush
|
||||||
|
# Replace with a path that contains/will contain your working directory
|
||||||
|
# - /path/to/a/workdir:/var/lib/platypush
|
||||||
|
# Optionally, use an external volume for the cache
|
||||||
|
# - /path/to/a/cachedir:/var/cache/platypush
|
||||||
|
# This may be required, together with privileged mode, if you want the
|
||||||
|
# container to access the USB bus on the host (required for example if
|
||||||
|
# you have USB audio devices that you want to access from your plugins,
|
||||||
|
# or Bluetooth dongles, or other USB adapters).
|
||||||
|
# - /dev/bus/usb:/dev/bus/usb
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis
|
|
@ -159,7 +159,7 @@ class IntegrationEnricher:
|
||||||
base_path,
|
base_path,
|
||||||
*doc.split(os.sep)[:-1],
|
*doc.split(os.sep)[:-1],
|
||||||
*doc.split(os.sep)[-1].split('.'),
|
*doc.split(os.sep)[-1].split('.'),
|
||||||
'manifest.yaml',
|
'manifest.json',
|
||||||
)
|
)
|
||||||
|
|
||||||
if not os.path.isfile(manifest_file):
|
if not os.path.isfile(manifest_file):
|
||||||
|
|
|
@ -152,7 +152,7 @@ const generateComponentsGrid = () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.location.pathname.endsWith('/index.html')) {
|
if (window.location.pathname === '/' || window.location.pathname.endsWith('/index.html')) {
|
||||||
if (tocWrappers.length < 2) {
|
if (tocWrappers.length < 2) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -188,9 +188,62 @@ const renderActionsList = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createFilterBar = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
const referenceSection = document.getElementById('reference')
|
||||||
|
|
||||||
|
input.type = 'text'
|
||||||
|
input.placeholder = 'Filter'
|
||||||
|
input.classList.add('filter-bar')
|
||||||
|
input.addEventListener('input', (event) => {
|
||||||
|
const filter = event.target.value.toLowerCase()
|
||||||
|
referenceSection.querySelectorAll('ul.grid li').forEach((li) => {
|
||||||
|
if (li.innerText.toLowerCase().includes(filter)) {
|
||||||
|
li.style.display = 'flex'
|
||||||
|
} else {
|
||||||
|
li.style.display = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFilterBar = () => {
|
||||||
|
const container = document.querySelector('.bd-main')
|
||||||
|
if (!container)
|
||||||
|
return
|
||||||
|
|
||||||
|
const referenceSection = document.getElementById('reference')
|
||||||
|
if (!referenceSection)
|
||||||
|
return
|
||||||
|
|
||||||
|
const header = referenceSection.querySelector('h2')
|
||||||
|
if (!header)
|
||||||
|
return
|
||||||
|
|
||||||
|
const origInnerHTML = header.innerHTML
|
||||||
|
header.innerHTML = '<span class="header-content">' + origInnerHTML + '</span>'
|
||||||
|
|
||||||
|
const input = createFilterBar()
|
||||||
|
header.appendChild(input)
|
||||||
|
|
||||||
|
const headerOffsetTop = header.offsetTop
|
||||||
|
|
||||||
|
// Apply the fixed class if the header is above the viewport
|
||||||
|
document.addEventListener('scroll', () => {
|
||||||
|
if (headerOffsetTop < window.scrollY) {
|
||||||
|
header.classList.add('fixed')
|
||||||
|
} else {
|
||||||
|
header.classList.remove('fixed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
generateComponentsGrid()
|
generateComponentsGrid()
|
||||||
convertDepsToTabs()
|
convertDepsToTabs()
|
||||||
addClipboardToCodeBlocks()
|
addClipboardToCodeBlocks()
|
||||||
renderActionsList()
|
renderActionsList()
|
||||||
|
addFilterBar()
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,15 +29,18 @@ a.grid-title {
|
||||||
|
|
||||||
ul.grid li {
|
ul.grid li {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
background: linear-gradient(0deg, #fff, #f9f9f9);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
margin: 0 10px 10px 0;
|
margin: 0 10px 10px 0;
|
||||||
padding: 10px;
|
padding: 20px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.grid img {
|
ul.grid img {
|
||||||
width: 32px;
|
width: 48px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,13 +55,20 @@ ul.grid li code .pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.grid li:hover {
|
ul.grid li:hover {
|
||||||
background: linear-gradient(0deg, #e0ffe8, #e3ffff);
|
background: linear-gradient(0deg, #157765, #cbffd8) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.grid li a {
|
ul.grid li a {
|
||||||
width: calc(100% - 35px);
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.grid li:hover a,
|
||||||
|
ul.grid li:hover a > code {
|
||||||
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.grid li a code {
|
ul.grid li a code {
|
||||||
|
@ -128,3 +138,51 @@ ul.grid .icon {
|
||||||
border-radius: 0 0 0.75em 0.75em;
|
border-radius: 0 0 0.75em 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bd-article-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.6em;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 0.75em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reference h2.fixed {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
background: white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reference h2.fixed .header-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
#reference h2.fixed {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: -0.5em;
|
||||||
|
padding: 0.5em 0.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 959px) {
|
||||||
|
#reference h2.fixed {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: -1em;
|
||||||
|
padding: 0.5em 0.5em 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 960px) {
|
||||||
|
#reference h2.fixed {
|
||||||
|
width: 75%;
|
||||||
|
max-width: 800px;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,9 +7,6 @@ Backends
|
||||||
:caption: Backends:
|
:caption: Backends:
|
||||||
|
|
||||||
platypush/backend/http.rst
|
platypush/backend/http.rst
|
||||||
platypush/backend/midi.rst
|
|
||||||
platypush/backend/nodered.rst
|
platypush/backend/nodered.rst
|
||||||
platypush/backend/redis.rst
|
platypush/backend/redis.rst
|
||||||
platypush/backend/stt.picovoice.hotword.rst
|
|
||||||
platypush/backend/stt.picovoice.speech.rst
|
|
||||||
platypush/backend/tcp.rst
|
platypush/backend/tcp.rst
|
||||||
|
|
|
@ -21,7 +21,7 @@ sys.path.insert(0, os.path.abspath("./_ext"))
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'Platypush'
|
project = 'Platypush'
|
||||||
copyright = '2017-2023, Fabio Manganiello'
|
copyright = '2017-2024, Fabio Manganiello'
|
||||||
author = 'Fabio Manganiello <fabio@manganiello.tech>'
|
author = 'Fabio Manganiello <fabio@manganiello.tech>'
|
||||||
|
|
||||||
# The short X.Y version
|
# The short X.Y version
|
||||||
|
@ -199,6 +199,7 @@ intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
|
||||||
autodoc_default_options = {
|
autodoc_default_options = {
|
||||||
'members': True,
|
'members': True,
|
||||||
'show-inheritance': True,
|
'show-inheritance': True,
|
||||||
|
'inherited-members': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
sys.path.insert(0, os.path.abspath('../..'))
|
||||||
|
|
|
@ -24,7 +24,6 @@ Events
|
||||||
platypush/events/geo.rst
|
platypush/events/geo.rst
|
||||||
platypush/events/github.rst
|
platypush/events/github.rst
|
||||||
platypush/events/google.rst
|
platypush/events/google.rst
|
||||||
platypush/events/google.fit.rst
|
|
||||||
platypush/events/google.pubsub.rst
|
platypush/events/google.pubsub.rst
|
||||||
platypush/events/gotify.rst
|
platypush/events/gotify.rst
|
||||||
platypush/events/gpio.rst
|
platypush/events/gpio.rst
|
||||||
|
@ -74,7 +73,6 @@ Events
|
||||||
platypush/events/web.rst
|
platypush/events/web.rst
|
||||||
platypush/events/web.widget.rst
|
platypush/events/web.widget.rst
|
||||||
platypush/events/websocket.rst
|
platypush/events/websocket.rst
|
||||||
platypush/events/wiimote.rst
|
|
||||||
platypush/events/xmpp.rst
|
platypush/events/xmpp.rst
|
||||||
platypush/events/zeroborg.rst
|
platypush/events/zeroborg.rst
|
||||||
platypush/events/zeroconf.rst
|
platypush/events/zeroconf.rst
|
||||||
|
|
|
@ -24,9 +24,8 @@ Useful links
|
||||||
============
|
============
|
||||||
|
|
||||||
* The `main page <https://platypush.tech>`_ of the project.
|
* The `main page <https://platypush.tech>`_ of the project.
|
||||||
* The `Gitea page <https://git.platypush.tech/platypush/platypush>`_.
|
* The `source code <https://git.platypush.tech/platypush/platypush>`_.
|
||||||
* The `blog <https://blog.platypush.tech>`_, for articles showing how to use
|
* The `blog <https://blog.platypush.tech>`_.
|
||||||
Platypush in real-world scenarios.
|
|
||||||
|
|
||||||
Wiki
|
Wiki
|
||||||
====
|
====
|
||||||
|
@ -34,11 +33,15 @@ Wiki
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 3
|
:maxdepth: 3
|
||||||
|
|
||||||
wiki/index
|
wiki/Home
|
||||||
|
wiki/Quickstart
|
||||||
wiki/Installation
|
wiki/Installation
|
||||||
|
wiki/Plugins-installation
|
||||||
|
wiki/APIs
|
||||||
|
wiki/Variables
|
||||||
|
wiki/Entities
|
||||||
wiki/Configuration
|
wiki/Configuration
|
||||||
wiki/Installing-extensions
|
wiki/A-full-configuration-example
|
||||||
wiki/A-configuration-example
|
|
||||||
wiki/The-Web-interface
|
wiki/The-Web-interface
|
||||||
|
|
||||||
Reference
|
Reference
|
||||||
|
@ -50,7 +53,6 @@ Reference
|
||||||
backends
|
backends
|
||||||
plugins
|
plugins
|
||||||
events
|
events
|
||||||
responses
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``midi``
|
|
||||||
==========================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.midi
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
``stt.picovoice.hotword``
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.stt.picovoice.hotword
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``stt.picovoice.speech``
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.backend.stt.picovoice.speech
|
|
||||||
:members:
|
|
|
@ -1,6 +0,0 @@
|
||||||
``google.fit``
|
|
||||||
======================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.google.fit
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``wiimote``
|
|
||||||
===================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.event.wiimote
|
|
||||||
:members:
|
|
||||||
|
|
5
docs/source/platypush/plugins/assistant.openai.rst
Normal file
5
docs/source/platypush/plugins/assistant.openai.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``assistant.openai``
|
||||||
|
====================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.assistant.openai
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/assistant.picovoice.rst
Normal file
5
docs/source/platypush/plugins/assistant.picovoice.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``assistant.picovoice``
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.assistant.picovoice
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/cups.rst
Normal file
5
docs/source/platypush/plugins/cups.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``cups``
|
||||||
|
========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.cups
|
||||||
|
:members:
|
|
@ -1,6 +0,0 @@
|
||||||
``google.fit``
|
|
||||||
================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.google.fit
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
``media.omxplayer``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.media.omxplayer
|
|
||||||
:members:
|
|
||||||
|
|
5
docs/source/platypush/plugins/openai.rst
Normal file
5
docs/source/platypush/plugins/openai.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``openai``
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.openai
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``printer.cups``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.printer.cups
|
|
||||||
:members:
|
|
5
docs/source/platypush/plugins/procedures.rst
Normal file
5
docs/source/platypush/plugins/procedures.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``procedures``
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.procedures
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``stt.picovoice.hotword``
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.stt.picovoice.hotword
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``stt.picovoice.speech``
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.plugins.stt.picovoice.speech
|
|
||||||
:members:
|
|
5
docs/source/platypush/plugins/tts.openai.rst
Normal file
5
docs/source/platypush/plugins/tts.openai.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``tts.openai``
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.tts.openai
|
||||||
|
:members:
|
5
docs/source/platypush/plugins/tts.picovoice.rst
Normal file
5
docs/source/platypush/plugins/tts.picovoice.rst
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
``tts.picovoice``
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. automodule:: platypush.plugins.tts.picovoice
|
||||||
|
:members:
|
|
@ -1,5 +0,0 @@
|
||||||
``camera.android``
|
|
||||||
=============================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.camera.android
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``camera``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.camera
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``google.drive``
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.google.drive
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``pihole``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.pihole
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``printer.cups``
|
|
||||||
===========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.printer.cups
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``qrcode``
|
|
||||||
=====================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.qrcode
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``ssh``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.ssh
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``stt``
|
|
||||||
==================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.stt
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``tensorflow``
|
|
||||||
=========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.tensorflow
|
|
||||||
:members:
|
|
|
@ -1,5 +0,0 @@
|
||||||
``translate``
|
|
||||||
========================================
|
|
||||||
|
|
||||||
.. automodule:: platypush.message.response.translate
|
|
||||||
:members:
|
|
|
@ -11,6 +11,8 @@ Plugins
|
||||||
platypush/plugins/application.rst
|
platypush/plugins/application.rst
|
||||||
platypush/plugins/arduino.rst
|
platypush/plugins/arduino.rst
|
||||||
platypush/plugins/assistant.google.rst
|
platypush/plugins/assistant.google.rst
|
||||||
|
platypush/plugins/assistant.openai.rst
|
||||||
|
platypush/plugins/assistant.picovoice.rst
|
||||||
platypush/plugins/autoremote.rst
|
platypush/plugins/autoremote.rst
|
||||||
platypush/plugins/bluetooth.rst
|
platypush/plugins/bluetooth.rst
|
||||||
platypush/plugins/calendar.rst
|
platypush/plugins/calendar.rst
|
||||||
|
@ -25,6 +27,7 @@ Plugins
|
||||||
platypush/plugins/clipboard.rst
|
platypush/plugins/clipboard.rst
|
||||||
platypush/plugins/config.rst
|
platypush/plugins/config.rst
|
||||||
platypush/plugins/csv.rst
|
platypush/plugins/csv.rst
|
||||||
|
platypush/plugins/cups.rst
|
||||||
platypush/plugins/db.rst
|
platypush/plugins/db.rst
|
||||||
platypush/plugins/dbus.rst
|
platypush/plugins/dbus.rst
|
||||||
platypush/plugins/dropbox.rst
|
platypush/plugins/dropbox.rst
|
||||||
|
@ -38,7 +41,6 @@ Plugins
|
||||||
platypush/plugins/github.rst
|
platypush/plugins/github.rst
|
||||||
platypush/plugins/google.calendar.rst
|
platypush/plugins/google.calendar.rst
|
||||||
platypush/plugins/google.drive.rst
|
platypush/plugins/google.drive.rst
|
||||||
platypush/plugins/google.fit.rst
|
|
||||||
platypush/plugins/google.mail.rst
|
platypush/plugins/google.mail.rst
|
||||||
platypush/plugins/google.maps.rst
|
platypush/plugins/google.maps.rst
|
||||||
platypush/plugins/google.pubsub.rst
|
platypush/plugins/google.pubsub.rst
|
||||||
|
@ -75,7 +77,6 @@ Plugins
|
||||||
platypush/plugins/media.kodi.rst
|
platypush/plugins/media.kodi.rst
|
||||||
platypush/plugins/media.mplayer.rst
|
platypush/plugins/media.mplayer.rst
|
||||||
platypush/plugins/media.mpv.rst
|
platypush/plugins/media.mpv.rst
|
||||||
platypush/plugins/media.omxplayer.rst
|
|
||||||
platypush/plugins/media.plex.rst
|
platypush/plugins/media.plex.rst
|
||||||
platypush/plugins/media.subtitles.rst
|
platypush/plugins/media.subtitles.rst
|
||||||
platypush/plugins/media.vlc.rst
|
platypush/plugins/media.vlc.rst
|
||||||
|
@ -94,10 +95,11 @@ Plugins
|
||||||
platypush/plugins/ngrok.rst
|
platypush/plugins/ngrok.rst
|
||||||
platypush/plugins/nmap.rst
|
platypush/plugins/nmap.rst
|
||||||
platypush/plugins/ntfy.rst
|
platypush/plugins/ntfy.rst
|
||||||
|
platypush/plugins/openai.rst
|
||||||
platypush/plugins/otp.rst
|
platypush/plugins/otp.rst
|
||||||
platypush/plugins/pihole.rst
|
platypush/plugins/pihole.rst
|
||||||
platypush/plugins/ping.rst
|
platypush/plugins/ping.rst
|
||||||
platypush/plugins/printer.cups.rst
|
platypush/plugins/procedures.rst
|
||||||
platypush/plugins/pushbullet.rst
|
platypush/plugins/pushbullet.rst
|
||||||
platypush/plugins/pwm.pca9685.rst
|
platypush/plugins/pwm.pca9685.rst
|
||||||
platypush/plugins/qrcode.rst
|
platypush/plugins/qrcode.rst
|
||||||
|
@ -119,8 +121,6 @@ Plugins
|
||||||
platypush/plugins/smartthings.rst
|
platypush/plugins/smartthings.rst
|
||||||
platypush/plugins/sound.rst
|
platypush/plugins/sound.rst
|
||||||
platypush/plugins/ssh.rst
|
platypush/plugins/ssh.rst
|
||||||
platypush/plugins/stt.picovoice.hotword.rst
|
|
||||||
platypush/plugins/stt.picovoice.speech.rst
|
|
||||||
platypush/plugins/sun.rst
|
platypush/plugins/sun.rst
|
||||||
platypush/plugins/switch.tplink.rst
|
platypush/plugins/switch.tplink.rst
|
||||||
platypush/plugins/switch.wemo.rst
|
platypush/plugins/switch.wemo.rst
|
||||||
|
@ -135,6 +135,8 @@ Plugins
|
||||||
platypush/plugins/tts.rst
|
platypush/plugins/tts.rst
|
||||||
platypush/plugins/tts.google.rst
|
platypush/plugins/tts.google.rst
|
||||||
platypush/plugins/tts.mimic3.rst
|
platypush/plugins/tts.mimic3.rst
|
||||||
|
platypush/plugins/tts.openai.rst
|
||||||
|
platypush/plugins/tts.picovoice.rst
|
||||||
platypush/plugins/tv.samsung.ws.rst
|
platypush/plugins/tv.samsung.ws.rst
|
||||||
platypush/plugins/twilio.rst
|
platypush/plugins/twilio.rst
|
||||||
platypush/plugins/udp.rst
|
platypush/plugins/udp.rst
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
|
|
||||||
Responses
|
|
||||||
=========
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
:caption: Responses:
|
|
||||||
|
|
||||||
platypush/responses/camera.rst
|
|
||||||
platypush/responses/camera.android.rst
|
|
||||||
platypush/responses/google.drive.rst
|
|
||||||
platypush/responses/pihole.rst
|
|
||||||
platypush/responses/printer.cups.rst
|
|
||||||
platypush/responses/qrcode.rst
|
|
||||||
platypush/responses/ssh.rst
|
|
||||||
platypush/responses/stt.rst
|
|
||||||
platypush/responses/tensorflow.rst
|
|
||||||
platypush/responses/translate.rst
|
|
|
@ -1,15 +1,16 @@
|
||||||
# A more versatile way to define event hooks than the YAML format of `config.yaml` is through native Python scripts.
|
# A more versatile way to define event hooks than the YAML format of
|
||||||
# You can define hooks as simple Python functions that use the `platypush.event.hook.hook` decorator to specify on
|
# `config.yaml` is through native Python scripts. You can define hooks as simple
|
||||||
# which event type they should be called, and optionally on which event attribute values.
|
# Python functions that use the `platypush.event.hook.hook` decorator to specify
|
||||||
|
# on which event type they should be called, and optionally on which event
|
||||||
|
# attribute values.
|
||||||
#
|
#
|
||||||
# Event hooks should be stored in Python files under `~/.config/platypush/scripts`. All the functions that use the
|
# Event hooks should be stored in Python files under
|
||||||
# @hook decorator will automatically be discovered and imported as event hooks into the platform at runtime.
|
# `~/.config/platypush/scripts`. All the functions that use the @when decorator
|
||||||
|
# will automatically be discovered and imported as event hooks into the platform
|
||||||
|
# at runtime.
|
||||||
|
|
||||||
# `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
|
# `run` is a utility function that runs a request by name (e.g. `light.hue.on`).
|
||||||
from platypush.utils import run
|
from platypush import when, run
|
||||||
|
|
||||||
# @hook decorator
|
|
||||||
from platypush.event.hook import hook
|
|
||||||
|
|
||||||
# Event types that you want to react to
|
# Event types that you want to react to
|
||||||
from platypush.message.event.assistant import (
|
from platypush.message.event.assistant import (
|
||||||
|
@ -18,13 +19,15 @@ from platypush.message.event.assistant import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@hook(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
|
@when(SpeechRecognizedEvent, phrase='play ${title} by ${artist}')
|
||||||
def on_music_play_command(event, title=None, artist=None, **context):
|
def on_music_play_command(event, title=None, artist=None):
|
||||||
"""
|
"""
|
||||||
This function will be executed when a SpeechRecognizedEvent with `phrase="play the music"` is triggered.
|
This function will be executed when a SpeechRecognizedEvent with
|
||||||
`event` contains the event object and `context` any key-value info from the running context.
|
`phrase="play the music"` is triggered. `event` contains the event object
|
||||||
Note that in this specific case we can leverage the token-extraction feature of SpeechRecognizedEvent through
|
and `context` any key-value info from the running context. Note that in this
|
||||||
${} that operates on regex-like principles to extract any text that matches the pattern into context variables.
|
specific case we can leverage the token-extraction feature of
|
||||||
|
SpeechRecognizedEvent through ${} that operates on regex-like principles to
|
||||||
|
extract any text that matches the pattern into context variables.
|
||||||
"""
|
"""
|
||||||
results = run(
|
results = run(
|
||||||
'music.mpd.search',
|
'music.mpd.search',
|
||||||
|
@ -34,16 +37,17 @@ def on_music_play_command(event, title=None, artist=None, **context):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if results:
|
if results and results[0]:
|
||||||
run('music.mpd.play', results[0]['file'])
|
run('music.mpd.play', results[0]['file'])
|
||||||
else:
|
else:
|
||||||
run('tts.say', "I can't find any music matching your query")
|
run('tts.say', "I can't find any music matching your query")
|
||||||
|
|
||||||
|
|
||||||
@hook(ConversationStartEvent)
|
@when(ConversationStartEvent)
|
||||||
def on_conversation_start(event, **context):
|
def on_conversation_start():
|
||||||
"""
|
"""
|
||||||
A simple hook that gets invoked when a new conversation starts with a voice assistant and simply pauses the music
|
A simple hook that gets invoked when a new conversation starts with a voice
|
||||||
to make sure that your speech is properly detected.
|
assistant and simply pauses the music to make sure that your speech is
|
||||||
|
properly detected.
|
||||||
"""
|
"""
|
||||||
run('music.mpd.pause_if_playing')
|
run('music.mpd.pause_if_playing')
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
# platypush systemd service example.
|
|
||||||
# Edit and copy this file to your systemd folder. It's usually
|
|
||||||
# /usr/lib/systemd/user for global installation or
|
|
||||||
# ~/.config/systemd/user for user installation. You can
|
|
||||||
# then control and monitor the service through
|
|
||||||
# systemd [--user] [start|stop|restart|status] platypush.service
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Platypush daemon
|
|
||||||
After=network.target redis.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
# platypush installation path
|
|
||||||
ExecStart=/usr/bin/platypush
|
|
||||||
Restart=always
|
|
||||||
# How long should be waited before restarting the service
|
|
||||||
# in case of failure.
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
1
examples/systemd/platypush.service
Symbolic link
1
examples/systemd/platypush.service
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../platypush/config/systemd/platypush.service
|
|
@ -8,7 +8,6 @@ import pkgutil
|
||||||
|
|
||||||
from platypush.backend import Backend
|
from platypush.backend import Backend
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
from platypush.message.response import Response
|
|
||||||
from platypush.plugins import Plugin
|
from platypush.plugins import Plugin
|
||||||
from platypush.utils.manifest import Manifests
|
from platypush.utils.manifest import Manifests
|
||||||
from platypush.utils.mock import auto_mocks
|
from platypush.utils.mock import auto_mocks
|
||||||
|
@ -26,10 +25,6 @@ def get_all_events():
|
||||||
return _get_modules(Event)
|
return _get_modules(Event)
|
||||||
|
|
||||||
|
|
||||||
def get_all_responses():
|
|
||||||
return _get_modules(Response)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_modules(base_type: type):
|
def _get_modules(base_type: type):
|
||||||
ret = set()
|
ret = set()
|
||||||
base_dir = os.path.dirname(inspect.getfile(base_type))
|
base_dir = os.path.dirname(inspect.getfile(base_type))
|
||||||
|
@ -151,20 +146,11 @@ def generate_events_doc():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_responses_doc():
|
|
||||||
_generate_components_doc(
|
|
||||||
index_name='responses',
|
|
||||||
package_name='message.response',
|
|
||||||
components=sorted(response for response in get_all_responses() if response),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with auto_mocks():
|
with auto_mocks():
|
||||||
generate_plugins_doc()
|
generate_plugins_doc()
|
||||||
generate_backends_doc()
|
generate_backends_doc()
|
||||||
generate_events_doc()
|
generate_events_doc()
|
||||||
generate_responses_doc()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -5,29 +5,42 @@ Platypush
|
||||||
.. license: MIT
|
.. license: MIT
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .app import Application
|
from .app import Application, app
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .context import get_backend, get_bus, get_plugin
|
from .context import Variable, get_backend, get_bus, get_plugin
|
||||||
|
from .cron import cron
|
||||||
|
from .event.hook import hook
|
||||||
from .message.event import Event
|
from .message.event import Event
|
||||||
from .message.request import Request
|
from .message.request import Request
|
||||||
from .message.response import Response
|
from .message.response import Response
|
||||||
|
from .procedure import procedure
|
||||||
from .runner import main
|
from .runner import main
|
||||||
from .utils import run
|
from .utils import run
|
||||||
|
|
||||||
|
# Alias for platypush.event.hook.hook,
|
||||||
|
# see https://git.platypush.tech/platypush/platypush/issues/399
|
||||||
|
when = hook
|
||||||
|
|
||||||
|
__version__ = '1.3.4'
|
||||||
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
|
__author__ = 'Fabio Manganiello <fabio@manganiello.tech>'
|
||||||
__version__ = '0.50.3'
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Application',
|
'Application',
|
||||||
|
'Variable',
|
||||||
'Config',
|
'Config',
|
||||||
'Event',
|
'Event',
|
||||||
'Request',
|
'Request',
|
||||||
'Response',
|
'Response',
|
||||||
|
'app',
|
||||||
|
'cron',
|
||||||
'get_backend',
|
'get_backend',
|
||||||
'get_bus',
|
'get_bus',
|
||||||
'get_plugin',
|
'get_plugin',
|
||||||
|
'hook',
|
||||||
'main',
|
'main',
|
||||||
|
'procedure',
|
||||||
'run',
|
'run',
|
||||||
|
'when',
|
||||||
|
'__version__',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from ._app import Application, main
|
from ._app import Application, app, main
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Application", "main"]
|
__all__ = ["Application", "app", "main"]
|
||||||
|
|
|
@ -5,6 +5,7 @@ import os
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
from platypush.bus import Bus
|
from platypush.bus import Bus
|
||||||
|
@ -18,7 +19,6 @@ from platypush.entities import init_entities_engine, EntitiesEngine
|
||||||
from platypush.event.processor import EventProcessor
|
from platypush.event.processor import EventProcessor
|
||||||
from platypush.logger import Logger
|
from platypush.logger import Logger
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
from platypush.message.event.application import ApplicationStartedEvent
|
|
||||||
from platypush.message.request import Request
|
from platypush.message.request import Request
|
||||||
from platypush.message.response import Response
|
from platypush.message.response import Response
|
||||||
from platypush.utils import get_enabled_plugins, get_redis_conf
|
from platypush.utils import get_enabled_plugins, get_redis_conf
|
||||||
|
@ -32,6 +32,9 @@ class Application:
|
||||||
# Default Redis port
|
# Default Redis port
|
||||||
_default_redis_port = 6379
|
_default_redis_port = 6379
|
||||||
|
|
||||||
|
# Default Redis binary, if --start-redis is set
|
||||||
|
_default_redis_bin = 'redis-server'
|
||||||
|
|
||||||
# backend_name => backend_obj map
|
# backend_name => backend_obj map
|
||||||
backends = None
|
backends = None
|
||||||
|
|
||||||
|
@ -55,6 +58,7 @@ class Application:
|
||||||
start_redis: bool = False,
|
start_redis: bool = False,
|
||||||
redis_host: Optional[str] = None,
|
redis_host: Optional[str] = None,
|
||||||
redis_port: Optional[int] = None,
|
redis_port: Optional[int] = None,
|
||||||
|
redis_bin: Optional[str] = None,
|
||||||
ctrl_sock: Optional[str] = None,
|
ctrl_sock: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -142,10 +146,11 @@ class Application:
|
||||||
:param verbose: Enable debug/verbose logging, overriding the stored
|
:param verbose: Enable debug/verbose logging, overriding the stored
|
||||||
configuration (default: False).
|
configuration (default: False).
|
||||||
:param start_redis: If set, it starts a managed Redis instance upon
|
:param start_redis: If set, it starts a managed Redis instance upon
|
||||||
boot (it requires the ``redis-server`` executable installed on the
|
boot (it requires Redis installed on the server, see
|
||||||
server). This is particularly useful when running the application
|
``redis_bin``). This is particularly useful when running the
|
||||||
inside of Docker containers, without relying on ``docker-compose``
|
application inside of Docker containers, without relying on
|
||||||
to start multiple containers, and in tests (default: False).
|
``docker-compose`` to start multiple containers, and in tests
|
||||||
|
(default: False).
|
||||||
:param redis_host: Host of the Redis server to be used. The order of
|
:param redis_host: Host of the Redis server to be used. The order of
|
||||||
precedence is:
|
precedence is:
|
||||||
|
|
||||||
|
@ -168,6 +173,16 @@ class Application:
|
||||||
the configuration file.
|
the configuration file.
|
||||||
- ``6379``
|
- ``6379``
|
||||||
|
|
||||||
|
:param redis_bin: Path to the Redis server executable, if ``start_redis``
|
||||||
|
is set. Alternative drop-in Redis implementations such as
|
||||||
|
``keydb-server``, ``valkey``, ``redict`` can be used. The order of
|
||||||
|
precedence is:
|
||||||
|
|
||||||
|
- The ``redis_bin`` parameter (or the ``--redis-bin`` command
|
||||||
|
line argument).
|
||||||
|
- The ``PLATYPUSH_REDIS_BIN`` environment variable.
|
||||||
|
- ``redis-server``
|
||||||
|
|
||||||
:param ctrl_sock: If set, it identifies a path to a UNIX domain socket
|
:param ctrl_sock: If set, it identifies a path to a UNIX domain socket
|
||||||
that the application can use to send control messages (e.g. STOP
|
that the application can use to send control messages (e.g. STOP
|
||||||
and RESTART) to its parent.
|
and RESTART) to its parent.
|
||||||
|
@ -180,6 +195,8 @@ class Application:
|
||||||
or os.environ.get('PLATYPUSH_REDIS_QUEUE')
|
or os.environ.get('PLATYPUSH_REDIS_QUEUE')
|
||||||
or RedisBus.DEFAULT_REDIS_QUEUE
|
or RedisBus.DEFAULT_REDIS_QUEUE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
os.environ['PLATYPUSH_REDIS_QUEUE'] = self.redis_queue
|
||||||
self.config_file = config_file or os.environ.get('PLATYPUSH_CONFIG')
|
self.config_file = config_file or os.environ.get('PLATYPUSH_CONFIG')
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.db_engine = db or os.environ.get('PLATYPUSH_DB')
|
self.db_engine = db or os.environ.get('PLATYPUSH_DB')
|
||||||
|
@ -209,6 +226,11 @@ class Application:
|
||||||
self.start_redis = start_redis
|
self.start_redis = start_redis
|
||||||
self.redis_host = redis_host or os.environ.get('PLATYPUSH_REDIS_HOST')
|
self.redis_host = redis_host or os.environ.get('PLATYPUSH_REDIS_HOST')
|
||||||
self.redis_port = redis_port or os.environ.get('PLATYPUSH_REDIS_PORT')
|
self.redis_port = redis_port or os.environ.get('PLATYPUSH_REDIS_PORT')
|
||||||
|
self.redis_bin = (
|
||||||
|
redis_bin
|
||||||
|
or os.environ.get('PLATYPUSH_REDIS_BIN')
|
||||||
|
or self._default_redis_bin
|
||||||
|
)
|
||||||
self._redis_conf = {
|
self._redis_conf = {
|
||||||
'host': self.redis_host or 'localhost',
|
'host': self.redis_host or 'localhost',
|
||||||
'port': self.redis_port or self._default_redis_port,
|
'port': self.redis_port or self._default_redis_port,
|
||||||
|
@ -260,7 +282,7 @@ class Application:
|
||||||
port = self._redis_conf['port']
|
port = self._redis_conf['port']
|
||||||
log.info('Starting local Redis instance on %s', port)
|
log.info('Starting local Redis instance on %s', port)
|
||||||
redis_cmd_args = [
|
redis_cmd_args = [
|
||||||
'redis-server',
|
self.redis_bin,
|
||||||
'--bind',
|
'--bind',
|
||||||
'localhost',
|
'localhost',
|
||||||
'--port',
|
'--port',
|
||||||
|
@ -343,7 +365,13 @@ class Application:
|
||||||
elif isinstance(msg, Response):
|
elif isinstance(msg, Response):
|
||||||
msg.log()
|
msg.log()
|
||||||
elif isinstance(msg, Event):
|
elif isinstance(msg, Event):
|
||||||
msg.log()
|
log.info(
|
||||||
|
'Received event: %s.%s[id=%s]',
|
||||||
|
msg.__class__.__module__,
|
||||||
|
msg.__class__.__name__,
|
||||||
|
msg.id,
|
||||||
|
)
|
||||||
|
msg.log(level=logging.DEBUG)
|
||||||
self.event_processor.process_event(msg)
|
self.event_processor.process_event(msg)
|
||||||
|
|
||||||
return _f
|
return _f
|
||||||
|
@ -420,7 +448,21 @@ class Application:
|
||||||
if not self.no_capture_stderr:
|
if not self.no_capture_stderr:
|
||||||
sys.stderr = Logger(log.warning)
|
sys.stderr = Logger(log.warning)
|
||||||
|
|
||||||
log.info('---- Starting platypush v.%s', __version__)
|
log.info(
|
||||||
|
dedent(
|
||||||
|
r'''
|
||||||
|
_____ _ _ _
|
||||||
|
| __ \| | | | | |
|
||||||
|
| |__) | | __ _| |_ _ _ _ __ _ _ ___| |__
|
||||||
|
| ___/| |/ _` | __| | | | '_ \| | | / __| '_ \
|
||||||
|
| | | | (_| | |_| |_| | |_) | |_| \__ \ | | |
|
||||||
|
|_| |_|\__,_|\__|\__, | .__/ \__,_|___/_| |_|
|
||||||
|
__/ | |
|
||||||
|
|___/|_|
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
log.info('---- Starting Platypush v.%s', __version__)
|
||||||
|
|
||||||
# Start the local Redis service if required
|
# Start the local Redis service if required
|
||||||
if self.start_redis:
|
if self.start_redis:
|
||||||
|
@ -445,7 +487,6 @@ class Application:
|
||||||
self.cron_scheduler.start()
|
self.cron_scheduler.start()
|
||||||
|
|
||||||
assert self.bus, 'The bus is not running'
|
assert self.bus, 'The bus is not running'
|
||||||
self.bus.post(ApplicationStartedEvent())
|
|
||||||
|
|
||||||
# Poll for messages on the bus
|
# Poll for messages on the bus
|
||||||
try:
|
try:
|
||||||
|
@ -464,10 +505,15 @@ class Application:
|
||||||
self._run()
|
self._run()
|
||||||
|
|
||||||
|
|
||||||
|
app: Optional[Application] = None
|
||||||
|
|
||||||
|
|
||||||
def main(*args: str):
|
def main(*args: str):
|
||||||
"""
|
"""
|
||||||
Application entry point.
|
Application entry point.
|
||||||
"""
|
"""
|
||||||
|
global app
|
||||||
|
|
||||||
app = Application.from_cmdline(args)
|
app = Application.from_cmdline(args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -402,6 +402,13 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.zeroconf:
|
||||||
|
self.logger.info(
|
||||||
|
'Zeroconf service already registered for %s, removing the previous instance',
|
||||||
|
self.__class__.__name__,
|
||||||
|
)
|
||||||
|
self.unregister_service()
|
||||||
|
|
||||||
self.zeroconf = Zeroconf()
|
self.zeroconf = Zeroconf()
|
||||||
srv_desc = {
|
srv_desc = {
|
||||||
'name': 'Platypush',
|
'name': 'Platypush',
|
||||||
|
|
|
@ -10,10 +10,8 @@ from multiprocessing import Process
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Mapping, Optional
|
from typing import Mapping, Optional
|
||||||
|
|
||||||
import psutil
|
|
||||||
|
|
||||||
from tornado.httpserver import HTTPServer
|
from tornado.httpserver import HTTPServer
|
||||||
from tornado.netutil import bind_sockets
|
from tornado.netutil import bind_sockets, bind_unix_socket
|
||||||
from tornado.process import cpu_count, fork_processes
|
from tornado.process import cpu_count, fork_processes
|
||||||
from tornado.wsgi import WSGIContainer
|
from tornado.wsgi import WSGIContainer
|
||||||
from tornado.web import Application, FallbackHandler
|
from tornado.web import Application, FallbackHandler
|
||||||
|
@ -153,14 +151,13 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from platypush.context import get_plugin
|
from platypush import get_plugin, when
|
||||||
from platypush.event.hook import hook
|
|
||||||
from platypush.message.event.http.hook import WebhookEvent
|
from platypush.message.event.http.hook import WebhookEvent
|
||||||
|
|
||||||
hook_token = 'abcdefabcdef'
|
hook_token = 'abcdefabcdef'
|
||||||
|
|
||||||
# Expose the hook under the /hook/lights_toggle endpoint
|
# Expose the hook under the /hook/lights_toggle endpoint
|
||||||
@hook(WebhookEvent, hook='lights_toggle')
|
@when(WebhookEvent, hook='lights_toggle')
|
||||||
def lights_toggle(event, **context):
|
def lights_toggle(event, **context):
|
||||||
# Do any checks on the request
|
# Do any checks on the request
|
||||||
assert event.headers.get('X-Token') == hook_token, 'Unauthorized'
|
assert event.headers.get('X-Token') == hook_token, 'Unauthorized'
|
||||||
|
@ -201,7 +198,8 @@ class HttpBackend(Backend):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
port: int = DEFAULT_HTTP_PORT,
|
port: int = DEFAULT_HTTP_PORT,
|
||||||
bind_address: str = '0.0.0.0',
|
bind_address: Optional[str] = '0.0.0.0',
|
||||||
|
bind_socket: Optional[str] = None,
|
||||||
resource_dirs: Optional[Mapping[str, str]] = None,
|
resource_dirs: Optional[Mapping[str, str]] = None,
|
||||||
secret_key_file: Optional[str] = None,
|
secret_key_file: Optional[str] = None,
|
||||||
num_workers: Optional[int] = None,
|
num_workers: Optional[int] = None,
|
||||||
|
@ -210,7 +208,16 @@ class HttpBackend(Backend):
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param port: Listen port for the web server (default: 8008)
|
:param port: Listen port for the web server (default: 8008)
|
||||||
:param bind_address: Address/interface to bind to (default: 0.0.0.0, accept connection from any IP)
|
:param bind_address: Address/interface to bind to (default: 0.0.0.0,
|
||||||
|
accept connection from any IP). You can set it to null to disable
|
||||||
|
the network interface binding, but then you must set ``bind_socket``
|
||||||
|
as an alternative.
|
||||||
|
:param bind_socket: Path to the Unix socket to bind to. If set, the
|
||||||
|
server will bind to the path of the specified Unix socket. If set to
|
||||||
|
``true``, then a socket will be automatically initialized on
|
||||||
|
``<workdir>/platypush-<device_id>.sock``. If not set, the server will
|
||||||
|
only listen on the specified bind address and port. Note that either
|
||||||
|
``bind_socket`` or ``socket_path`` must be set.
|
||||||
:param resource_dirs: Static resources directories that will be
|
:param resource_dirs: Static resources directories that will be
|
||||||
accessible through ``/resources/<path>``. It is expressed as a map
|
accessible through ``/resources/<path>``. It is expressed as a map
|
||||||
where the key is the relative path under ``/resources`` to expose and
|
where the key is the relative path under ``/resources`` to expose and
|
||||||
|
@ -232,11 +239,23 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
assert (
|
||||||
|
bind_address or bind_socket
|
||||||
|
), 'Either bind_address or bind_socket must be set'
|
||||||
self.port = port
|
self.port = port
|
||||||
self._server_proc: Optional[Process] = None
|
self._server_proc: Optional[Process] = None
|
||||||
self._service_registry_thread = None
|
self._service_registry_thread = None
|
||||||
self.bind_address = bind_address
|
self.bind_address = bind_address
|
||||||
|
|
||||||
|
if bind_socket is True:
|
||||||
|
bind_socket = os.path.join(
|
||||||
|
Config.get_workdir(), f'platypush-{Config.get_device_id()}.sock'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.socket_path = None
|
||||||
|
if bind_socket:
|
||||||
|
self.socket_path = os.path.expanduser(bind_socket)
|
||||||
|
|
||||||
if resource_dirs:
|
if resource_dirs:
|
||||||
self.resource_dirs = {
|
self.resource_dirs = {
|
||||||
name: os.path.abspath(os.path.expanduser(d))
|
name: os.path.abspath(os.path.expanduser(d))
|
||||||
|
@ -261,8 +280,8 @@ class HttpBackend(Backend):
|
||||||
super().on_stop()
|
super().on_stop()
|
||||||
self.logger.info('Received STOP event on HttpBackend')
|
self.logger.info('Received STOP event on HttpBackend')
|
||||||
start = time()
|
start = time()
|
||||||
remaining_time: partial[float] = partial( # type: ignore
|
remaining_time: partial[float] = partial(
|
||||||
get_remaining_timeout, timeout=self._STOP_TIMEOUT, start=start
|
get_remaining_timeout, timeout=self._STOP_TIMEOUT, start=start # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._server_proc:
|
if self._server_proc:
|
||||||
|
@ -365,6 +384,7 @@ class HttpBackend(Backend):
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.use_werkzeug_server:
|
if self.use_werkzeug_server:
|
||||||
|
assert self.bind_address, 'bind_address must be set when using Werkzeug'
|
||||||
application.config['redis_queue'] = self.bus.redis_queue # type: ignore
|
application.config['redis_queue'] = self.bus.redis_queue # type: ignore
|
||||||
application.run(
|
application.run(
|
||||||
host=self.bind_address,
|
host=self.bind_address,
|
||||||
|
@ -373,9 +393,13 @@ class HttpBackend(Backend):
|
||||||
debug=True,
|
debug=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sockets = bind_sockets(
|
sockets = []
|
||||||
self.port, address=self.bind_address, reuse_port=True
|
|
||||||
)
|
if self.bind_address:
|
||||||
|
sockets.extend(bind_sockets(self.port, address=self.bind_address))
|
||||||
|
|
||||||
|
if self.socket_path:
|
||||||
|
sockets.append(bind_unix_socket(self.socket_path))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fork_processes(self.num_workers)
|
fork_processes(self.num_workers)
|
||||||
|
@ -395,6 +419,14 @@ class HttpBackend(Backend):
|
||||||
workers when the server terminates:
|
workers when the server terminates:
|
||||||
https://github.com/tornadoweb/tornado/issues/1912.
|
https://github.com/tornadoweb/tornado/issues/1912.
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
except ImportError:
|
||||||
|
self.logger.warning(
|
||||||
|
'Could not import psutil, hanging worker processes might remain active'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
parent_pid = (
|
parent_pid = (
|
||||||
self._server_proc.pid
|
self._server_proc.pid
|
||||||
if self._server_proc and self._server_proc.pid
|
if self._server_proc and self._server_proc.pid
|
||||||
|
@ -422,8 +454,8 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
# Initialize the timeout
|
# Initialize the timeout
|
||||||
start = time()
|
start = time()
|
||||||
remaining_time: partial[int] = partial( # type: ignore
|
remaining_time: partial[int] = partial(
|
||||||
get_remaining_timeout, timeout=self._STOP_TIMEOUT, start=start, cls=int
|
get_remaining_timeout, timeout=self._STOP_TIMEOUT, start=start, cls=int # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
# Wait for all children to terminate (with timeout)
|
# Wait for all children to terminate (with timeout)
|
||||||
|
|
|
@ -4,8 +4,15 @@ import logging
|
||||||
|
|
||||||
from flask import Blueprint, request, abort, jsonify
|
from flask import Blueprint, request, abort, jsonify
|
||||||
|
|
||||||
|
from platypush.backend.http.app.utils import authenticate
|
||||||
|
from platypush.backend.http.app.utils.auth import (
|
||||||
|
UserAuthStatus,
|
||||||
|
current_user,
|
||||||
|
get_current_user_or_auth_status,
|
||||||
|
)
|
||||||
from platypush.exceptions.user import UserException
|
from platypush.exceptions.user import UserException
|
||||||
from platypush.user import UserManager
|
from platypush.user import User, UserManager
|
||||||
|
from platypush.utils import utcnow
|
||||||
|
|
||||||
auth = Blueprint('auth', __name__)
|
auth = Blueprint('auth', __name__)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -16,39 +23,24 @@ __routes__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@auth.route('/auth', methods=['POST'])
|
def _dump_session(session, redirect_page='/'):
|
||||||
def auth_endpoint():
|
return jsonify(
|
||||||
"""
|
|
||||||
Authentication endpoint. It validates the user credentials provided over a JSON payload with the following
|
|
||||||
structure:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"username": "USERNAME",
|
'status': 'ok',
|
||||||
"password": "PASSWORD",
|
'user_id': session.user_id,
|
||||||
"expiry_days": "The generated token should be valid for these many days"
|
'session_token': session.session_token,
|
||||||
|
'expires_at': session.expires_at,
|
||||||
|
'redirect': redirect_page,
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
``expiry_days`` is optional, and if omitted or set to zero the token will be valid indefinitely.
|
|
||||||
|
|
||||||
Upon successful validation, a new JWT token will be generated using the service's self-generated RSA key-pair and it
|
def _jwt_auth():
|
||||||
will be returned to the user. The token can then be used to authenticate API calls to ``/execute`` by setting the
|
|
||||||
``Authorization: Bearer <TOKEN_HERE>`` header upon HTTP calls.
|
|
||||||
|
|
||||||
:return: Return structure:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"token": "<generated token here>"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
payload = json.loads(request.get_data(as_text=True))
|
payload = json.loads(request.get_data(as_text=True))
|
||||||
username, password = payload['username'], payload['password']
|
username, password = payload['username'], payload['password']
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.warning('Invalid payload passed to the auth endpoint: ' + str(e))
|
log.warning('Invalid payload passed to the auth endpoint')
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
expiry_days = payload.get('expiry_days')
|
expiry_days = payload.get('expiry_days')
|
||||||
|
@ -59,8 +51,365 @@ def auth_endpoint():
|
||||||
user_manager = UserManager()
|
user_manager = UserManager()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return jsonify({
|
return jsonify(
|
||||||
'token': user_manager.generate_jwt_token(username=username, password=password, expires_at=expires_at),
|
{
|
||||||
})
|
'token': user_manager.generate_jwt_token(
|
||||||
|
username=username, password=password, expires_at=expires_at
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
except UserException as e:
|
except UserException as e:
|
||||||
abort(401, str(e))
|
abort(401, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def _session_auth():
|
||||||
|
user_manager = UserManager()
|
||||||
|
session_token = request.cookies.get('session_token')
|
||||||
|
redirect_page = request.args.get('redirect') or '/'
|
||||||
|
|
||||||
|
if session_token:
|
||||||
|
user, session = user_manager.authenticate_user_session(session_token)[:2]
|
||||||
|
if user and session:
|
||||||
|
return _dump_session(session, redirect_page)
|
||||||
|
|
||||||
|
if request.form:
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
code = request.form.get('code')
|
||||||
|
remember = request.form.get('remember')
|
||||||
|
expires = utcnow() + datetime.timedelta(days=365) if remember else None
|
||||||
|
session, status = user_manager.create_user_session( # type: ignore
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
code=code,
|
||||||
|
expires_at=expires,
|
||||||
|
with_status=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return _dump_session(session, redirect_page)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
auth_status = UserAuthStatus.by_status(status)
|
||||||
|
assert auth_status
|
||||||
|
return auth_status.to_response()
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_token():
|
||||||
|
payload = {}
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.get_data(as_text=True))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
user = None
|
||||||
|
username = payload.get('username')
|
||||||
|
password = payload.get('password')
|
||||||
|
name = payload.get('name')
|
||||||
|
expiry_days = payload.get('expiry_days')
|
||||||
|
user_manager = UserManager()
|
||||||
|
response = get_current_user_or_auth_status(request)
|
||||||
|
|
||||||
|
# Try and authenticate with the credentials passed in the JSON payload
|
||||||
|
if username and password:
|
||||||
|
user = user_manager.authenticate_user(username, password, skip_2fa=True)
|
||||||
|
if not isinstance(user, User):
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
if not (response and isinstance(response, User)):
|
||||||
|
return response.to_response()
|
||||||
|
|
||||||
|
user = response
|
||||||
|
|
||||||
|
expires_at = None
|
||||||
|
if expiry_days:
|
||||||
|
expires_at = datetime.datetime.now() + datetime.timedelta(days=expiry_days)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = UserManager().generate_api_token(
|
||||||
|
username=str(user.username), name=name, expires_at=expires_at
|
||||||
|
)
|
||||||
|
return jsonify({'token': token})
|
||||||
|
except UserException:
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_token():
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.get_data(as_text=True))
|
||||||
|
token = payload.get('token')
|
||||||
|
assert token
|
||||||
|
except (AssertionError, json.JSONDecodeError):
|
||||||
|
return UserAuthStatus.INVALID_TOKEN.to_response()
|
||||||
|
|
||||||
|
user_manager = UserManager()
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = payload.get('token')
|
||||||
|
if not token:
|
||||||
|
return UserAuthStatus.INVALID_TOKEN.to_response()
|
||||||
|
|
||||||
|
ret = user_manager.delete_api_token(token)
|
||||||
|
if not ret:
|
||||||
|
return UserAuthStatus.INVALID_TOKEN.to_response()
|
||||||
|
|
||||||
|
return jsonify({'status': 'ok'})
|
||||||
|
except UserException:
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _register_route():
|
||||||
|
"""Registration endpoint"""
|
||||||
|
user_manager = UserManager()
|
||||||
|
session_token = request.cookies.get('session_token')
|
||||||
|
redirect_page = request.args.get('redirect') or '/'
|
||||||
|
|
||||||
|
if session_token:
|
||||||
|
user, session = user_manager.authenticate_user_session(session_token)[:2]
|
||||||
|
if user and session:
|
||||||
|
return _dump_session(session, redirect_page)
|
||||||
|
|
||||||
|
if user_manager.get_user_count() > 0:
|
||||||
|
return UserAuthStatus.REGISTRATION_DISABLED.to_response()
|
||||||
|
|
||||||
|
if not request.form:
|
||||||
|
return UserAuthStatus.MISSING_USERNAME.to_response()
|
||||||
|
|
||||||
|
username = request.form.get('username')
|
||||||
|
password = request.form.get('password')
|
||||||
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
remember = request.form.get('remember')
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return UserAuthStatus.MISSING_USERNAME.to_response()
|
||||||
|
if not password:
|
||||||
|
return UserAuthStatus.MISSING_PASSWORD.to_response()
|
||||||
|
if password != confirm_password:
|
||||||
|
return UserAuthStatus.PASSWORD_MISMATCH.to_response()
|
||||||
|
|
||||||
|
user_manager.create_user(username=username, password=password)
|
||||||
|
session, status = user_manager.create_user_session( # type: ignore
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
expires_at=(utcnow() + datetime.timedelta(days=365) if remember else None),
|
||||||
|
with_status=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
return _dump_session(session, redirect_page)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
return status.to_response() # type: ignore
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _auth_get():
|
||||||
|
"""
|
||||||
|
Get the current authentication status of the user session.
|
||||||
|
"""
|
||||||
|
user_manager = UserManager()
|
||||||
|
session_token = request.cookies.get('session_token')
|
||||||
|
redirect_page = request.args.get('redirect') or '/'
|
||||||
|
user, session, status = user_manager.authenticate_user_session( # type: ignore
|
||||||
|
session_token, with_status=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if user and session:
|
||||||
|
return _dump_session(session, redirect_page)
|
||||||
|
|
||||||
|
response = get_current_user_or_auth_status(request)
|
||||||
|
if isinstance(response, User):
|
||||||
|
user = response
|
||||||
|
return jsonify(
|
||||||
|
{'status': 'ok', 'user_id': user.user_id, 'username': user.username}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response:
|
||||||
|
status = response
|
||||||
|
|
||||||
|
if status:
|
||||||
|
if not isinstance(status, UserAuthStatus):
|
||||||
|
status = UserAuthStatus.by_status(status)
|
||||||
|
if not status:
|
||||||
|
status = UserAuthStatus.INVALID_CREDENTIALS
|
||||||
|
return status.to_response()
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _auth_post():
|
||||||
|
"""
|
||||||
|
Authenticate the user session.
|
||||||
|
"""
|
||||||
|
auth_type = request.args.get('type') or 'token'
|
||||||
|
|
||||||
|
if auth_type == 'token':
|
||||||
|
return _create_token()
|
||||||
|
|
||||||
|
if auth_type == 'jwt':
|
||||||
|
return _jwt_auth()
|
||||||
|
|
||||||
|
if auth_type == 'register':
|
||||||
|
return _register_route()
|
||||||
|
|
||||||
|
if auth_type == 'login':
|
||||||
|
return _session_auth()
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_AUTH_TYPE.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _auth_delete():
|
||||||
|
"""
|
||||||
|
Logout/invalidate a token or the current user session.
|
||||||
|
"""
|
||||||
|
# Delete the specified API token if it's passed on the JSON payload
|
||||||
|
token = None
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.get_data(as_text=True))
|
||||||
|
token = payload.get('token')
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if token:
|
||||||
|
return _delete_token()
|
||||||
|
|
||||||
|
user_manager = UserManager()
|
||||||
|
session_token = request.cookies.get('session_token')
|
||||||
|
redirect_page = request.args.get('redirect') or '/'
|
||||||
|
|
||||||
|
if session_token:
|
||||||
|
user, session = user_manager.authenticate_user_session(session_token)[:2]
|
||||||
|
if user and session:
|
||||||
|
user_manager.delete_user_session(session_token)
|
||||||
|
return jsonify({'status': 'ok', 'redirect': redirect_page})
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_SESSION.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
def _tokens_get():
|
||||||
|
user = current_user()
|
||||||
|
if not user:
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
tokens = UserManager().get_api_tokens(username=str(user.username))
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
'tokens': [
|
||||||
|
{
|
||||||
|
'id': t.id,
|
||||||
|
'name': t.name,
|
||||||
|
'created_at': t.created_at,
|
||||||
|
'expires_at': t.expires_at,
|
||||||
|
}
|
||||||
|
for t in tokens
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _tokens_delete():
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.get_data(as_text=True))
|
||||||
|
token = payload.get('token')
|
||||||
|
if token:
|
||||||
|
args['token'] = token
|
||||||
|
else:
|
||||||
|
token_id = payload.get('token_id')
|
||||||
|
if token_id:
|
||||||
|
args['token_id'] = token_id
|
||||||
|
|
||||||
|
assert args, 'No token or token_id specified'
|
||||||
|
except (AssertionError, json.JSONDecodeError):
|
||||||
|
return UserAuthStatus.INVALID_TOKEN.to_response()
|
||||||
|
|
||||||
|
user_manager = UserManager()
|
||||||
|
user = current_user()
|
||||||
|
if not user:
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
|
||||||
|
args['username'] = str(user.username)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_manager.delete_api_token(**args)
|
||||||
|
return jsonify({'status': 'ok'})
|
||||||
|
except AssertionError as e:
|
||||||
|
return (
|
||||||
|
jsonify({'status': 'error', 'error': 'bad_request', 'message': str(e)}),
|
||||||
|
400,
|
||||||
|
)
|
||||||
|
except UserException:
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Token deletion error', exc_info=e)
|
||||||
|
|
||||||
|
return UserAuthStatus.UNKNOWN_ERROR.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
@auth.route('/auth', methods=['GET', 'POST', 'DELETE'])
|
||||||
|
def auth_endpoint():
|
||||||
|
"""
|
||||||
|
Authentication endpoint. It validates the user credentials provided over a
|
||||||
|
JSON payload with the following structure:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "USERNAME",
|
||||||
|
"password": "PASSWORD",
|
||||||
|
"code": "2FA_CODE",
|
||||||
|
"expiry_days": "The generated token should be valid for these many days"
|
||||||
|
}
|
||||||
|
|
||||||
|
``expiry_days`` is optional, and if omitted or set to zero the token will
|
||||||
|
be valid indefinitely.
|
||||||
|
|
||||||
|
Upon successful validation, a new JWT token will be generated using the
|
||||||
|
service's self-generated RSA key-pair and it will be returned to the user.
|
||||||
|
The token can then be used to authenticate API calls to ``/execute`` by
|
||||||
|
setting the ``Authorization: Bearer <TOKEN_HERE>`` header upon HTTP calls.
|
||||||
|
|
||||||
|
:return: Return structure:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "<generated token here>"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if request.method == 'GET':
|
||||||
|
return _auth_get()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
return _auth_post()
|
||||||
|
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
return _auth_delete()
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_METHOD.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
@auth.route('/tokens', methods=['GET', 'DELETE'])
|
||||||
|
@authenticate()
|
||||||
|
def tokens_route():
|
||||||
|
"""
|
||||||
|
:return: The list of API tokens created by the logged in user.
|
||||||
|
Note that this endpoint is only accessible by authenticated users
|
||||||
|
and it won't return the clear-text token values, as those aren't
|
||||||
|
stored in the database anyway.
|
||||||
|
"""
|
||||||
|
if request.method == 'GET':
|
||||||
|
return _tokens_get()
|
||||||
|
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
return _tokens_delete()
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_METHOD.to_response()
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -14,9 +14,27 @@ __routes__ = [
|
||||||
|
|
||||||
@index.route('/')
|
@index.route('/')
|
||||||
@authenticate()
|
@authenticate()
|
||||||
def index():
|
def index_route():
|
||||||
"""Route to the main web panel"""
|
"""Route to the main web panel"""
|
||||||
return render_template('index.html', utils=HttpUtils)
|
return render_template('index.html', utils=HttpUtils)
|
||||||
|
|
||||||
|
|
||||||
|
@index.route('/login', methods=['GET'])
|
||||||
|
def login_route():
|
||||||
|
"""
|
||||||
|
Login GET route. It simply renders the index template, which will
|
||||||
|
redirect to the login page if the user is not authenticated.
|
||||||
|
"""
|
||||||
|
return render_template('index.html', utils=HttpUtils)
|
||||||
|
|
||||||
|
|
||||||
|
@index.route('/register', methods=['GET'])
|
||||||
|
def register_route():
|
||||||
|
"""
|
||||||
|
Register GET route. It simply renders the index template, which will
|
||||||
|
redirect to the registration page if no users are present.
|
||||||
|
"""
|
||||||
|
return render_template('index.html', utils=HttpUtils)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, render_template, make_response
|
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
|
||||||
from platypush.user import UserManager
|
|
||||||
|
|
||||||
login = Blueprint('login', __name__, template_folder=template_folder)
|
|
||||||
|
|
||||||
# Declare routes list
|
|
||||||
__routes__ = [
|
|
||||||
login,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@login.route('/login', methods=['GET', 'POST'])
|
|
||||||
def login():
|
|
||||||
""" Login page """
|
|
||||||
user_manager = UserManager()
|
|
||||||
session_token = request.cookies.get('session_token')
|
|
||||||
|
|
||||||
redirect_page = request.args.get('redirect')
|
|
||||||
if not redirect_page:
|
|
||||||
redirect_page = request.headers.get('Referer', '/')
|
|
||||||
if re.search('(^https?://[^/]+)?/login[^?#]?', redirect_page):
|
|
||||||
# Prevent redirect loop
|
|
||||||
redirect_page = '/'
|
|
||||||
|
|
||||||
if session_token:
|
|
||||||
user, session = user_manager.authenticate_user_session(session_token)
|
|
||||||
if user:
|
|
||||||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
|
||||||
|
|
||||||
if request.form:
|
|
||||||
username = request.form.get('username')
|
|
||||||
password = request.form.get('password')
|
|
||||||
remember = request.form.get('remember')
|
|
||||||
expires = datetime.datetime.utcnow() + datetime.timedelta(days=365) \
|
|
||||||
if remember else None
|
|
||||||
|
|
||||||
session = user_manager.create_user_session(username=username, password=password,
|
|
||||||
expires_at=expires)
|
|
||||||
|
|
||||||
if session:
|
|
||||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
|
||||||
response = make_response(redirect_target)
|
|
||||||
response.set_cookie('session_token', session.session_token, expires=expires)
|
|
||||||
return response
|
|
||||||
|
|
||||||
return render_template('index.html', utils=HttpUtils)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -12,7 +12,7 @@ __routes__ = [
|
||||||
|
|
||||||
|
|
||||||
@logout.route('/logout', methods=['GET', 'POST'])
|
@logout.route('/logout', methods=['GET', 'POST'])
|
||||||
def logout():
|
def logout_route():
|
||||||
"""Logout page"""
|
"""Logout page"""
|
||||||
user_manager = UserManager()
|
user_manager = UserManager()
|
||||||
redirect_page = request.args.get(
|
redirect_page = request.args.get(
|
||||||
|
@ -23,7 +23,7 @@ def logout():
|
||||||
if not session_token:
|
if not session_token:
|
||||||
abort(417, 'Not logged in')
|
abort(417, 'Not logged in')
|
||||||
|
|
||||||
user, _ = user_manager.authenticate_user_session(session_token)
|
user, _ = user_manager.authenticate_user_session(session_token)[:2]
|
||||||
if not user:
|
if not user:
|
||||||
abort(403, 'Invalid session token')
|
abort(403, 'Invalid session token')
|
||||||
|
|
||||||
|
|
220
platypush/backend/http/app/routes/otp.py
Normal file
220
platypush/backend/http/app/routes/otp.py
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
|
from platypush.backend.http.app import template_folder
|
||||||
|
from platypush.backend.http.app.utils import UserAuthStatus, authenticate
|
||||||
|
from platypush.backend.http.utils import HttpUtils
|
||||||
|
from platypush.exceptions.user import (
|
||||||
|
InvalidCredentialsException,
|
||||||
|
InvalidOtpCodeException,
|
||||||
|
UserException,
|
||||||
|
)
|
||||||
|
from platypush.config import Config
|
||||||
|
from platypush.context import get_plugin
|
||||||
|
from platypush.user import UserManager
|
||||||
|
|
||||||
|
otp = Blueprint('otp', __name__, template_folder=template_folder)
|
||||||
|
|
||||||
|
# Declare routes list
|
||||||
|
__routes__ = [
|
||||||
|
otp,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_otp_and_qrcode():
|
||||||
|
otp = get_plugin('otp') # pylint: disable=redefined-outer-name
|
||||||
|
qrcode = get_plugin('qrcode')
|
||||||
|
assert (
|
||||||
|
otp and qrcode
|
||||||
|
), 'The otp and/or qrcode plugins are not available in your installation'
|
||||||
|
|
||||||
|
return otp, qrcode
|
||||||
|
|
||||||
|
|
||||||
|
def _get_username():
|
||||||
|
user = HttpUtils.current_user()
|
||||||
|
if not user:
|
||||||
|
raise InvalidCredentialsException('Invalid user session')
|
||||||
|
|
||||||
|
return str(user.username)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_otp_uri_and_qrcode(username: str, otp_secret: Optional[str] = None):
|
||||||
|
if not otp_secret:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
otp, qrcode = _get_otp_and_qrcode() # pylint: disable=redefined-outer-name
|
||||||
|
otp_uri = (
|
||||||
|
otp.provision_time_otp(
|
||||||
|
name=username,
|
||||||
|
secret=otp_secret,
|
||||||
|
issuer=f'platypush@{Config.get_device_id()}',
|
||||||
|
).output
|
||||||
|
if otp_secret
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
otp_qrcode = (
|
||||||
|
qrcode.generate(content=otp_uri, format='png').output.get('data')
|
||||||
|
if otp_uri
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
return otp_uri, otp_qrcode
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_code(code: str, otp_secret: str) -> bool:
|
||||||
|
otp, _ = _get_otp_and_qrcode() # pylint: disable=redefined-outer-name
|
||||||
|
return otp.verify_time_otp(otp=code, secret=otp_secret).output
|
||||||
|
|
||||||
|
|
||||||
|
def _dump_response(
|
||||||
|
username: str,
|
||||||
|
otp_secret: Optional[str] = None,
|
||||||
|
backup_codes: Optional[List[str]] = None,
|
||||||
|
):
|
||||||
|
otp_uri, otp_qrcode = _get_otp_uri_and_qrcode(username, otp_secret)
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
'username': username,
|
||||||
|
'otp_secret': otp_secret,
|
||||||
|
'otp_uri': otp_uri,
|
||||||
|
'qrcode': otp_qrcode,
|
||||||
|
'backup_codes': backup_codes or [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_otp():
|
||||||
|
username = _get_username()
|
||||||
|
user_manager = UserManager()
|
||||||
|
otp_secret = user_manager.get_otp_secret(username)
|
||||||
|
return _dump_response(
|
||||||
|
username=username,
|
||||||
|
otp_secret=otp_secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _authenticate_user(username: str, password: Optional[str]):
|
||||||
|
assert password, 'The password field is required when setting up OTP'
|
||||||
|
user, auth_status = UserManager().authenticate_user( # type: ignore
|
||||||
|
username, password, skip_2fa=True, with_status=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise InvalidCredentialsException(auth_status.value[2])
|
||||||
|
|
||||||
|
|
||||||
|
def _post_otp():
|
||||||
|
body = request.json
|
||||||
|
assert body, 'Invalid request body'
|
||||||
|
|
||||||
|
username = _get_username()
|
||||||
|
dry_run = body.get('dry_run', False)
|
||||||
|
otp_secret = body.get('otp_secret')
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
_authenticate_user(username, body.get('password'))
|
||||||
|
|
||||||
|
if otp_secret:
|
||||||
|
code = body.get('code')
|
||||||
|
assert code, 'The code field is required when setting up OTP'
|
||||||
|
|
||||||
|
if not _verify_code(code, otp_secret):
|
||||||
|
raise InvalidOtpCodeException()
|
||||||
|
|
||||||
|
user_manager = UserManager()
|
||||||
|
user_otp, backup_codes = user_manager.enable_otp(
|
||||||
|
username=username,
|
||||||
|
otp_secret=otp_secret,
|
||||||
|
dry_run=dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _dump_response(
|
||||||
|
username=username,
|
||||||
|
otp_secret=str(user_otp.otp_secret),
|
||||||
|
backup_codes=backup_codes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_otp():
|
||||||
|
body = request.json
|
||||||
|
assert body, 'Invalid request body'
|
||||||
|
|
||||||
|
username = _get_username()
|
||||||
|
_authenticate_user(username, body.get('password'))
|
||||||
|
|
||||||
|
user_manager = UserManager()
|
||||||
|
user_manager.disable_otp(username)
|
||||||
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
|
@otp.route('/otp/config', methods=['GET', 'POST', 'DELETE'])
|
||||||
|
@authenticate()
|
||||||
|
def otp_route():
|
||||||
|
"""
|
||||||
|
:return: The user's current MFA/OTP configuration:
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "testuser",
|
||||||
|
"otp_secret": "JBSA6ZUZ5DPEK7YV",
|
||||||
|
"otp_uri": "otpauth://totp/testuser?secret=JBSA6ZUZ5DPEK7YV&issuer=platypush@localhost",
|
||||||
|
"qrcode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAABwklEQVR4nO3dMW7CQBAF0",
|
||||||
|
"backup_codes": [
|
||||||
|
"1A2B3C4D5E",
|
||||||
|
"6F7G8H9I0J",
|
||||||
|
"KLMNOPQRST",
|
||||||
|
"UVWXYZ1234",
|
||||||
|
"567890ABCD",
|
||||||
|
"EFGHIJKLMN",
|
||||||
|
"OPQRSTUVWX",
|
||||||
|
"YZ12345678",
|
||||||
|
"90ABCDEF12",
|
||||||
|
"34567890AB"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if request.method.lower() == 'get':
|
||||||
|
return _get_otp()
|
||||||
|
|
||||||
|
if request.method.lower() == 'post':
|
||||||
|
return _post_otp()
|
||||||
|
|
||||||
|
if request.method.lower() == 'delete':
|
||||||
|
return _delete_otp()
|
||||||
|
|
||||||
|
return jsonify({'error': 'Method not allowed'}), 405
|
||||||
|
except AssertionError as e:
|
||||||
|
return jsonify({'error': str(e)}), 400
|
||||||
|
except InvalidCredentialsException:
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS.to_response()
|
||||||
|
except InvalidOtpCodeException:
|
||||||
|
return UserAuthStatus.INVALID_OTP_CODE.to_response()
|
||||||
|
except UserException as e:
|
||||||
|
return jsonify({'error': e.__class__.__name__, 'message': str(e)}), 401
|
||||||
|
except Exception as e:
|
||||||
|
HttpUtils.log.error(f'Error while processing OTP request: {e}', exc_info=True)
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@otp.route('/otp/refresh-codes', methods=['POST'])
|
||||||
|
def refresh_codes():
|
||||||
|
"""
|
||||||
|
:return: A new set of backup codes for the user.
|
||||||
|
"""
|
||||||
|
username = _get_username()
|
||||||
|
user_manager = UserManager()
|
||||||
|
otp_secret = user_manager.get_otp_secret(username)
|
||||||
|
if not otp_secret:
|
||||||
|
return jsonify({'error': 'OTP not configured for the user'}), 400
|
||||||
|
|
||||||
|
backup_codes = user_manager.refresh_user_backup_codes(username)
|
||||||
|
return jsonify({'backup_codes': backup_codes})
|
||||||
|
|
||||||
|
|
||||||
|
# vim:sw=4:ts=4:et:
|
|
@ -1,4 +1,7 @@
|
||||||
from flask import Blueprint, jsonify, send_from_directory
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request, send_from_directory
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.backend.http.app import template_folder
|
from platypush.backend.http.app import template_folder
|
||||||
|
@ -11,13 +14,37 @@ __routes__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_plugin(url: Optional[str] = None) -> Optional[str]:
|
||||||
|
if not url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
path = urlparse(url).path.lstrip('/').split('/')
|
||||||
|
if len(path) > 1 and path[0] == 'plugin':
|
||||||
|
return path[1]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@pwa.route('/manifest.json', methods=['GET'])
|
@pwa.route('/manifest.json', methods=['GET'])
|
||||||
def manifest_json():
|
def manifest_json():
|
||||||
"""Generated manifest file for the PWA"""
|
"""Generated manifest file for the PWA"""
|
||||||
|
|
||||||
|
device_id = Config.get_device_id()
|
||||||
|
referer = request.headers.get('Referer')
|
||||||
|
plugin = _get_plugin(referer)
|
||||||
|
start_url = '/'
|
||||||
|
name = f'Platypush @ {device_id}'
|
||||||
|
short_name = device_id
|
||||||
|
|
||||||
|
if plugin:
|
||||||
|
start_url = f'/plugin/{plugin}'
|
||||||
|
name = f'{plugin} @ {device_id}'
|
||||||
|
short_name = plugin
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"name": f'Platypush @ {Config.get("device_id")}',
|
"name": name,
|
||||||
"short_name": Config.get('device_id'),
|
"short_name": short_name,
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/img/icons/favicon-16x16.png",
|
"src": "/img/icons/favicon-16x16.png",
|
||||||
|
@ -94,9 +121,9 @@ def manifest_json():
|
||||||
],
|
],
|
||||||
"gcm_sender_id": "",
|
"gcm_sender_id": "",
|
||||||
"gcm_user_visible_only": True,
|
"gcm_user_visible_only": True,
|
||||||
"start_url": "/",
|
"start_url": start_url,
|
||||||
"permissions": ["gcm"],
|
"permissions": ["gcm"],
|
||||||
"orientation": "portrait",
|
"orientation": "any",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
|
|
||||||
from flask import Blueprint, request, redirect, render_template, make_response, abort
|
|
||||||
|
|
||||||
from platypush.backend.http.app import template_folder
|
|
||||||
from platypush.backend.http.utils import HttpUtils
|
|
||||||
from platypush.user import UserManager
|
|
||||||
|
|
||||||
register = Blueprint('register', __name__, template_folder=template_folder)
|
|
||||||
|
|
||||||
# Declare routes list
|
|
||||||
__routes__ = [
|
|
||||||
register,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@register.route('/register', methods=['GET', 'POST'])
|
|
||||||
def register():
|
|
||||||
""" Registration page """
|
|
||||||
user_manager = UserManager()
|
|
||||||
redirect_page = request.args.get('redirect')
|
|
||||||
if not redirect_page:
|
|
||||||
redirect_page = request.headers.get('Referer', '/')
|
|
||||||
if re.search('(^https?://[^/]+)?/register[^?#]?', redirect_page):
|
|
||||||
# Prevent redirect loop
|
|
||||||
redirect_page = '/'
|
|
||||||
|
|
||||||
session_token = request.cookies.get('session_token')
|
|
||||||
|
|
||||||
if session_token:
|
|
||||||
user, session = user_manager.authenticate_user_session(session_token)
|
|
||||||
if user:
|
|
||||||
return redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
|
||||||
|
|
||||||
if user_manager.get_user_count() > 0:
|
|
||||||
return redirect('/login?redirect=' + redirect_page, 302) # lgtm [py/url-redirection]
|
|
||||||
|
|
||||||
if request.form:
|
|
||||||
username = request.form.get('username')
|
|
||||||
password = request.form.get('password')
|
|
||||||
confirm_password = request.form.get('confirm_password')
|
|
||||||
remember = request.form.get('remember')
|
|
||||||
|
|
||||||
if password == confirm_password:
|
|
||||||
user_manager.create_user(username=username, password=password)
|
|
||||||
session = user_manager.create_user_session(username=username, password=password,
|
|
||||||
expires_at=datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
|
||||||
if not remember else None)
|
|
||||||
|
|
||||||
if session:
|
|
||||||
redirect_target = redirect(redirect_page, 302) # lgtm [py/url-redirection]
|
|
||||||
response = make_response(redirect_target)
|
|
||||||
response.set_cookie('session_token', session.session_token)
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
abort(400, 'Password mismatch')
|
|
||||||
|
|
||||||
return render_template('index.html', utils=HttpUtils)
|
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
|
|
@ -7,7 +7,7 @@ from typing import Optional
|
||||||
from tornado.web import RequestHandler, stream_request_body
|
from tornado.web import RequestHandler, stream_request_body
|
||||||
|
|
||||||
from platypush.backend.http.app.utils import logger
|
from platypush.backend.http.app.utils import logger
|
||||||
from platypush.backend.http.app.utils.auth import AuthStatus, get_auth_status
|
from platypush.backend.http.app.utils.auth import UserAuthStatus, get_auth_status
|
||||||
|
|
||||||
from ..mixins import PubSubMixin
|
from ..mixins import PubSubMixin
|
||||||
|
|
||||||
|
@ -29,8 +29,9 @@ class StreamingRoute(RequestHandler, PubSubMixin, ABC):
|
||||||
"""
|
"""
|
||||||
if self.auth_required:
|
if self.auth_required:
|
||||||
auth_status = get_auth_status(self.request)
|
auth_status = get_auth_status(self.request)
|
||||||
if auth_status != AuthStatus.OK:
|
if auth_status != UserAuthStatus.OK:
|
||||||
self.send_error(auth_status.value.code, error=auth_status.value.message)
|
self.send_error(auth_status.value.code, error=auth_status.value.message)
|
||||||
|
self.finish()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
|
@ -42,6 +43,7 @@ class StreamingRoute(RequestHandler, PubSubMixin, ABC):
|
||||||
Make sure that errors are always returned in JSON format.
|
Make sure that errors are always returned in JSON format.
|
||||||
"""
|
"""
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
|
self.set_status(status_code)
|
||||||
self.finish(
|
self.finish(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{"status": status_code, "error": error or responses.get(status_code)}
|
{"status": status_code, "error": error or responses.get(status_code)}
|
||||||
|
|
205
platypush/backend/http/app/streaming/plugins/file.py
Normal file
205
platypush/backend/http/app/streaming/plugins/file.py
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime as dt
|
||||||
|
from typing import IO, Optional, Tuple
|
||||||
|
|
||||||
|
from tornado.web import stream_request_body
|
||||||
|
|
||||||
|
from platypush.utils import get_mime_type
|
||||||
|
|
||||||
|
from .. import StreamingRoute
|
||||||
|
|
||||||
|
|
||||||
|
@stream_request_body
|
||||||
|
class FileRoute(StreamingRoute):
|
||||||
|
"""
|
||||||
|
Generic route to read the content of a file on the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
BUFSIZE = 1024
|
||||||
|
_bytes_written = 0
|
||||||
|
_out_f: Optional[IO[bytes]] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def path(cls) -> str:
|
||||||
|
"""
|
||||||
|
Route: GET /file?path=<path>[&download]
|
||||||
|
"""
|
||||||
|
return r"^/file$"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def download(self) -> bool:
|
||||||
|
return 'download' in self.request.arguments
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_path(self) -> str:
|
||||||
|
return os.path.expanduser(
|
||||||
|
self.request.arguments.get('path', [b''])[0].decode('utf-8')
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_size(self) -> int:
|
||||||
|
return os.path.getsize(self.file_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _content_length(self) -> int:
|
||||||
|
return int(self.request.headers.get('Content-Length', 0))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def range(self) -> Tuple[Optional[int], Optional[int]]:
|
||||||
|
range_hdr = self.request.headers.get('Range')
|
||||||
|
if not range_hdr:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
start, end = range_hdr.split('=')[-1].split('-')
|
||||||
|
start = int(start) if start else 0
|
||||||
|
end = int(end) if end else self.file_size - 1
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
def set_headers(self):
|
||||||
|
self.set_header('Content-Length', str(os.path.getsize(self.file_path)))
|
||||||
|
self.set_header(
|
||||||
|
'Content-Type', get_mime_type(self.file_path) or 'application/octet-stream'
|
||||||
|
)
|
||||||
|
self.set_header('Accept-Ranges', 'bytes')
|
||||||
|
self.set_header(
|
||||||
|
'Last-Modified',
|
||||||
|
dt.fromtimestamp(os.path.getmtime(self.file_path)).strftime(
|
||||||
|
'%a, %d %b %Y %H:%M:%S GMT'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.download:
|
||||||
|
self.set_header(
|
||||||
|
'Content-Disposition',
|
||||||
|
f'attachment; filename="{os.path.basename(self.file_path)}"',
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.range[0] is not None:
|
||||||
|
start, end = self.range
|
||||||
|
self.set_header(
|
||||||
|
'Content-Range',
|
||||||
|
f'bytes {start}-{end}/{self.file_size}',
|
||||||
|
)
|
||||||
|
self.set_status(206)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _serve(self):
|
||||||
|
path = self.file_path
|
||||||
|
if not path:
|
||||||
|
self.write_error(400, 'Missing path argument')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.logger.debug('Received file read request for %r', path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
self.set_headers()
|
||||||
|
yield f
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.write_error(404, 'File not found')
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
except PermissionError:
|
||||||
|
self.write_error(403, 'Permission denied')
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.write_error(500, str(e))
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
self.finish()
|
||||||
|
|
||||||
|
def on_finish(self) -> None:
|
||||||
|
if self._out_f:
|
||||||
|
try:
|
||||||
|
if not (self._out_f and self._out_f.closed):
|
||||||
|
self._out_f.close()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Error while closing the output file: %s', e)
|
||||||
|
|
||||||
|
self._out_f = None
|
||||||
|
|
||||||
|
return super().on_finish()
|
||||||
|
|
||||||
|
def _validate_upload(self, force: bool = False) -> bool:
|
||||||
|
if not self.file_path:
|
||||||
|
self.write_error(400, 'Missing path argument')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self._out_f:
|
||||||
|
if not force and os.path.exists(self.file_path):
|
||||||
|
self.write_error(409, f'{self.file_path} already exists')
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._bytes_written = 0
|
||||||
|
dir_path = os.path.dirname(self.file_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pathlib.Path(dir_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
self._out_f = open( # pylint: disable=consider-using-with
|
||||||
|
self.file_path, 'wb'
|
||||||
|
)
|
||||||
|
except PermissionError:
|
||||||
|
self.write_error(403, 'Permission denied')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def finish(self, *args, **kwargs): # type: ignore
|
||||||
|
try:
|
||||||
|
return super().finish(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Error while finishing the request: %s', e)
|
||||||
|
|
||||||
|
def data_received(self, chunk: bytes):
|
||||||
|
# Ignore unless we're in POST/PUT mode
|
||||||
|
if self.request.method not in ('POST', 'PUT'):
|
||||||
|
return
|
||||||
|
|
||||||
|
force = self.request.method == 'PUT'
|
||||||
|
if not self._validate_upload(force=force):
|
||||||
|
self.finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not chunk:
|
||||||
|
self.logger.debug('Received EOF from client')
|
||||||
|
self.finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
assert self._out_f
|
||||||
|
self._out_f.write(chunk)
|
||||||
|
self._out_f.flush()
|
||||||
|
self._bytes_written += len(chunk)
|
||||||
|
self.logger.debug(
|
||||||
|
'Written chunk of size %d to %s, progress: %d/%d',
|
||||||
|
len(chunk),
|
||||||
|
self.file_path,
|
||||||
|
self._bytes_written,
|
||||||
|
self._content_length,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def get(self) -> None:
|
||||||
|
with self._serve() as f:
|
||||||
|
if f:
|
||||||
|
while True:
|
||||||
|
chunk = f.read(self.BUFSIZE)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.write(chunk)
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def head(self) -> None:
|
||||||
|
with self._serve():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post(self) -> None:
|
||||||
|
self.logger.info('Receiving file POST upload request for %r', self.file_path)
|
||||||
|
|
||||||
|
def put(self) -> None:
|
||||||
|
self.logger.info('Receiving file PUT upload request for %r', self.file_path)
|
|
@ -3,7 +3,9 @@ from typing import Optional
|
||||||
from platypush.backend.http.app.utils import logger, send_request
|
from platypush.backend.http.app.utils import logger, send_request
|
||||||
from platypush.backend.http.media.handlers import MediaHandler
|
from platypush.backend.http.media.handlers import MediaHandler
|
||||||
|
|
||||||
from ._registry import load_media_map, save_media_map
|
from ._registry import clear_media_map, load_media_map, save_media_map
|
||||||
|
|
||||||
|
_init = False
|
||||||
|
|
||||||
|
|
||||||
def get_media_url(media_id: str) -> str:
|
def get_media_url(media_id: str) -> str:
|
||||||
|
@ -17,6 +19,12 @@ def register_media(source: str, subtitles: Optional[str] = None) -> MediaHandler
|
||||||
"""
|
"""
|
||||||
Registers a media file and returns its associated media handler.
|
Registers a media file and returns its associated media handler.
|
||||||
"""
|
"""
|
||||||
|
global _init
|
||||||
|
|
||||||
|
if not _init:
|
||||||
|
clear_media_map()
|
||||||
|
_init = True
|
||||||
|
|
||||||
media_id = MediaHandler.get_media_id(source)
|
media_id = MediaHandler.get_media_id(source)
|
||||||
media_url = get_media_url(media_id)
|
media_url = get_media_url(media_id)
|
||||||
media_map = load_media_map()
|
media_map = load_media_map()
|
||||||
|
|
|
@ -25,10 +25,15 @@ def load_media_map() -> MediaMap:
|
||||||
logger().warning('Could not load media map: %s', e)
|
logger().warning('Could not load media map: %s', e)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return {
|
parsed_map = {}
|
||||||
media_id: MediaHandler.build(**media_info)
|
for media_id, media_info in media_map.items():
|
||||||
for media_id, media_info in media_map.items()
|
try:
|
||||||
}
|
parsed_map[media_id] = MediaHandler.build(**media_info)
|
||||||
|
except Exception as e:
|
||||||
|
logger().debug('Could not load media %s: %s', media_id, e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return parsed_map
|
||||||
|
|
||||||
|
|
||||||
def save_media_map(new_map: MediaMap):
|
def save_media_map(new_map: MediaMap):
|
||||||
|
@ -38,3 +43,12 @@ def save_media_map(new_map: MediaMap):
|
||||||
with media_map_lock:
|
with media_map_lock:
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
redis.mset({MEDIA_MAP_VAR: json.dumps(new_map, cls=Message.Encoder)})
|
redis.mset({MEDIA_MAP_VAR: json.dumps(new_map, cls=Message.Encoder)})
|
||||||
|
|
||||||
|
|
||||||
|
def clear_media_map():
|
||||||
|
"""
|
||||||
|
Clears the media map from the server.
|
||||||
|
"""
|
||||||
|
with media_map_lock:
|
||||||
|
redis = get_redis()
|
||||||
|
redis.delete(MEDIA_MAP_VAR)
|
||||||
|
|
|
@ -17,7 +17,7 @@ class MediaStreamRoute(StreamingRoute):
|
||||||
Route for media streams.
|
Route for media streams.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SUPPORTED_METHODS = ['GET', 'PUT', 'DELETE']
|
SUPPORTED_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE']
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -50,6 +50,23 @@ class MediaStreamRoute(StreamingRoute):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._on_error(e)
|
self._on_error(e)
|
||||||
|
|
||||||
|
def head(self, media_id: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Streams a media resource by ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not media_id:
|
||||||
|
self.finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Strip the extension
|
||||||
|
media_id = '.'.join(media_id.split('.')[:-1])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.stream_media(media_id, head=True)
|
||||||
|
except Exception as e:
|
||||||
|
self._on_error(e)
|
||||||
|
|
||||||
def put(self, *_, **__):
|
def put(self, *_, **__):
|
||||||
"""
|
"""
|
||||||
The `PUT` route is used to prepare a new media resource for streaming.
|
The `PUT` route is used to prepare a new media resource for streaming.
|
||||||
|
@ -93,10 +110,10 @@ class MediaStreamRoute(StreamingRoute):
|
||||||
"""
|
"""
|
||||||
Returns the list of registered media resources.
|
Returns the list of registered media resources.
|
||||||
"""
|
"""
|
||||||
self.add_header('Content-Type', 'application/json')
|
self.set_header('Content-Type', 'application/json')
|
||||||
self.finish(json.dumps([dict(media) for media in load_media_map().values()]))
|
self.finish(json.dumps([dict(media) for media in load_media_map().values()]))
|
||||||
|
|
||||||
def stream_media(self, media_id: str):
|
def stream_media(self, media_id: str, head: bool = False):
|
||||||
"""
|
"""
|
||||||
Route to stream a media file given its ID.
|
Route to stream a media file given its ID.
|
||||||
"""
|
"""
|
||||||
|
@ -107,11 +124,11 @@ class MediaStreamRoute(StreamingRoute):
|
||||||
range_hdr = self.request.headers.get('Range')
|
range_hdr = self.request.headers.get('Range')
|
||||||
content_length = media_hndl.content_length
|
content_length = media_hndl.content_length
|
||||||
|
|
||||||
self.add_header('Accept-Ranges', 'bytes')
|
self.set_header('Accept-Ranges', 'bytes')
|
||||||
self.add_header('Content-Type', media_hndl.mime_type)
|
self.set_header('Content-Type', media_hndl.mime_type)
|
||||||
|
|
||||||
if 'download' in self.request.arguments:
|
if 'download' in self.request.arguments:
|
||||||
self.add_header(
|
self.set_header(
|
||||||
'Content-Disposition',
|
'Content-Disposition',
|
||||||
'attachment'
|
'attachment'
|
||||||
+ ('; filename="{media_hndl.filename}"' if media_hndl.filename else ''),
|
+ ('; filename="{media_hndl.filename}"' if media_hndl.filename else ''),
|
||||||
|
@ -129,7 +146,7 @@ class MediaStreamRoute(StreamingRoute):
|
||||||
content_length = to_bytes - from_bytes
|
content_length = to_bytes - from_bytes
|
||||||
|
|
||||||
self.set_status(206)
|
self.set_status(206)
|
||||||
self.add_header(
|
self.set_header(
|
||||||
'Content-Range',
|
'Content-Range',
|
||||||
f'bytes {from_bytes}-{to_bytes}/{media_hndl.content_length}',
|
f'bytes {from_bytes}-{to_bytes}/{media_hndl.content_length}',
|
||||||
)
|
)
|
||||||
|
@ -137,7 +154,13 @@ class MediaStreamRoute(StreamingRoute):
|
||||||
from_bytes = 0
|
from_bytes = 0
|
||||||
to_bytes = STREAMING_BLOCK_SIZE
|
to_bytes = STREAMING_BLOCK_SIZE
|
||||||
|
|
||||||
self.add_header('Content-Length', str(content_length))
|
self.set_header('Content-Length', str(content_length))
|
||||||
|
|
||||||
|
if head:
|
||||||
|
self.flush()
|
||||||
|
self.finish()
|
||||||
|
return
|
||||||
|
|
||||||
for chunk in media_hndl.get_data(
|
for chunk in media_hndl.get_data(
|
||||||
from_bytes=from_bytes,
|
from_bytes=from_bytes,
|
||||||
to_bytes=to_bytes,
|
to_bytes=to_bytes,
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from .auth import (
|
from .auth import (
|
||||||
|
UserAuthStatus,
|
||||||
authenticate,
|
authenticate,
|
||||||
authenticate_token,
|
authenticate_token,
|
||||||
authenticate_user_pass,
|
authenticate_user_pass,
|
||||||
|
current_user,
|
||||||
get_auth_status,
|
get_auth_status,
|
||||||
)
|
)
|
||||||
from .bus import bus, send_message, send_request
|
from .bus import bus, send_message, send_request
|
||||||
|
@ -17,10 +19,12 @@ from .streaming import get_streaming_routes
|
||||||
from .ws import get_ws_routes
|
from .ws import get_ws_routes
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
'UserAuthStatus',
|
||||||
'authenticate',
|
'authenticate',
|
||||||
'authenticate_token',
|
'authenticate_token',
|
||||||
'authenticate_user_pass',
|
'authenticate_user_pass',
|
||||||
'bus',
|
'bus',
|
||||||
|
'current_user',
|
||||||
'get_auth_status',
|
'get_auth_status',
|
||||||
'get_http_port',
|
'get_http_port',
|
||||||
'get_ip_or_hostname',
|
'get_ip_or_hostname',
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import base64
|
import base64
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
from flask import request, redirect, jsonify
|
from flask import request, redirect
|
||||||
from flask.wrappers import Response
|
from flask.wrappers import Response
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.user import UserManager
|
from platypush.user import User, UserManager
|
||||||
|
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from .status import AuthStatus
|
from .status import UserAuthStatus
|
||||||
|
|
||||||
user_manager = UserManager()
|
user_manager = UserManager()
|
||||||
|
|
||||||
|
@ -41,8 +41,8 @@ def get_cookie(req, name: str) -> Optional[str]:
|
||||||
return cookie.value
|
return cookie.value
|
||||||
|
|
||||||
|
|
||||||
def authenticate_token(req):
|
def authenticate_token(req) -> Optional[User]:
|
||||||
token = Config.get('token')
|
global_token = Config.get('user.global_token')
|
||||||
user_token = None
|
user_token = None
|
||||||
|
|
||||||
if 'X-Token' in req.headers:
|
if 'X-Token' in req.headers:
|
||||||
|
@ -55,14 +55,27 @@ def authenticate_token(req):
|
||||||
user_token = get_arg(req, 'token')
|
user_token = get_arg(req, 'token')
|
||||||
|
|
||||||
if not user_token:
|
if not user_token:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_manager.validate_jwt_token(user_token)
|
# Stantard API token authentication
|
||||||
return True
|
return user_manager.validate_api_token(user_token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger().debug(str(e))
|
try:
|
||||||
return bool(token and user_token == token)
|
# Legacy JWT token authentication
|
||||||
|
return user_manager.validate_jwt_token(user_token)
|
||||||
|
except Exception as ee:
|
||||||
|
logger().debug(
|
||||||
|
'Invalid token. API token error: %s, JWT token error: %s', e, ee
|
||||||
|
)
|
||||||
|
|
||||||
|
# Legacy global token authentication.
|
||||||
|
# The global token should be specified in the configuration file,
|
||||||
|
# as a root parameter named `token`.
|
||||||
|
if bool(global_token and user_token == global_token):
|
||||||
|
return User(username='__token__', user_id=1)
|
||||||
|
|
||||||
|
logger().info(e)
|
||||||
|
|
||||||
|
|
||||||
def authenticate_user_pass(req):
|
def authenticate_user_pass(req):
|
||||||
|
@ -91,7 +104,7 @@ def authenticate_user_pass(req):
|
||||||
return user_manager.authenticate_user(username, password)
|
return user_manager.authenticate_user(username, password)
|
||||||
|
|
||||||
|
|
||||||
def authenticate_session(req):
|
def authenticate_session(req) -> Optional[User]:
|
||||||
user = None
|
user = None
|
||||||
|
|
||||||
# Check the X-Session-Token header
|
# Check the X-Session-Token header
|
||||||
|
@ -106,9 +119,9 @@ def authenticate_session(req):
|
||||||
user_session_token = get_cookie(req, 'session_token')
|
user_session_token = get_cookie(req, 'session_token')
|
||||||
|
|
||||||
if user_session_token:
|
if user_session_token:
|
||||||
user, _ = user_manager.authenticate_user_session(user_session_token)
|
user, _ = user_manager.authenticate_user_session(user_session_token)[:2]
|
||||||
|
|
||||||
return user is not None
|
return user
|
||||||
|
|
||||||
|
|
||||||
def authenticate(
|
def authenticate(
|
||||||
|
@ -128,18 +141,18 @@ def authenticate(
|
||||||
skip_auth_methods=skip_auth_methods,
|
skip_auth_methods=skip_auth_methods,
|
||||||
)
|
)
|
||||||
|
|
||||||
if auth_status == AuthStatus.OK:
|
if auth_status == UserAuthStatus.OK:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
if json:
|
if json:
|
||||||
return jsonify(auth_status.to_dict()), auth_status.value.code
|
return auth_status.to_response()
|
||||||
|
|
||||||
if auth_status == AuthStatus.NO_USERS:
|
if auth_status == UserAuthStatus.REGISTRATION_REQUIRED:
|
||||||
return redirect(
|
return redirect(
|
||||||
f'/register?redirect={redirect_page or request.url}', 307
|
f'/register?redirect={redirect_page or request.url}', 307
|
||||||
)
|
)
|
||||||
|
|
||||||
if auth_status == AuthStatus.UNAUTHORIZED:
|
if auth_status == UserAuthStatus.INVALID_CREDENTIALS:
|
||||||
return redirect(f'/login?redirect={redirect_page or request.url}', 307)
|
return redirect(f'/login?redirect={redirect_page or request.url}', 307)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -154,43 +167,67 @@ def authenticate(
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
def get_auth_status(req, skip_auth_methods=None) -> AuthStatus:
|
def get_current_user_or_auth_status(
|
||||||
|
req, skip_auth_methods=None
|
||||||
|
) -> Union[User, UserAuthStatus]:
|
||||||
"""
|
"""
|
||||||
Check against the available authentication methods (except those listed in
|
Returns the current user if authenticated, and the authentication status if
|
||||||
``skip_auth_methods``) if the user is properly authenticated.
|
``with_status`` is True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
n_users = user_manager.get_user_count()
|
n_users = user_manager.get_user_count()
|
||||||
skip_methods = skip_auth_methods or []
|
skip_methods = skip_auth_methods or []
|
||||||
|
|
||||||
# User/pass HTTP authentication
|
# User/pass HTTP authentication
|
||||||
http_auth_ok = True
|
http_auth_ok = True
|
||||||
if n_users > 0 and 'http' not in skip_methods:
|
if n_users > 0 and 'http' not in skip_methods:
|
||||||
http_auth_ok = authenticate_user_pass(req)
|
response = authenticate_user_pass(req)
|
||||||
if http_auth_ok:
|
if response:
|
||||||
return AuthStatus.OK
|
user = response[0] if isinstance(response, tuple) else response
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
|
||||||
# Token-based authentication
|
# Token-based authentication
|
||||||
token_auth_ok = True
|
token_auth_ok = True
|
||||||
if 'token' not in skip_methods:
|
if 'token' not in skip_methods:
|
||||||
token_auth_ok = authenticate_token(req)
|
user = authenticate_token(req)
|
||||||
if token_auth_ok:
|
if user:
|
||||||
return AuthStatus.OK
|
return user
|
||||||
|
|
||||||
# Session token based authentication
|
# Session token based authentication
|
||||||
session_auth_ok = True
|
session_auth_ok = True
|
||||||
if n_users > 0 and 'session' not in skip_methods:
|
if n_users > 0 and 'session' not in skip_methods:
|
||||||
return AuthStatus.OK if authenticate_session(req) else AuthStatus.UNAUTHORIZED
|
user = authenticate_session(req)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
|
||||||
|
return UserAuthStatus.INVALID_CREDENTIALS
|
||||||
|
|
||||||
# At least a user should be created before accessing an authenticated resource
|
# At least a user should be created before accessing an authenticated resource
|
||||||
if n_users == 0 and 'session' not in skip_methods:
|
if n_users == 0 and 'session' not in skip_methods:
|
||||||
return AuthStatus.NO_USERS
|
return UserAuthStatus.REGISTRATION_REQUIRED
|
||||||
|
|
||||||
if ( # pylint: disable=too-many-boolean-expressions
|
if ( # pylint: disable=too-many-boolean-expressions
|
||||||
('http' not in skip_methods and http_auth_ok)
|
('http' not in skip_methods and http_auth_ok)
|
||||||
or ('token' not in skip_methods and token_auth_ok)
|
or ('token' not in skip_methods and token_auth_ok)
|
||||||
or ('session' not in skip_methods and session_auth_ok)
|
or ('session' not in skip_methods and session_auth_ok)
|
||||||
):
|
):
|
||||||
return AuthStatus.OK
|
return UserAuthStatus.OK
|
||||||
|
|
||||||
return AuthStatus.UNAUTHORIZED
|
return UserAuthStatus.INVALID_CREDENTIALS
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_status(req, skip_auth_methods=None) -> UserAuthStatus:
|
||||||
|
"""
|
||||||
|
Check against the available authentication methods (except those listed in
|
||||||
|
``skip_auth_methods``) if the user is properly authenticated.
|
||||||
|
"""
|
||||||
|
ret = get_current_user_or_auth_status(req, skip_auth_methods=skip_auth_methods)
|
||||||
|
return UserAuthStatus.OK if isinstance(ret, User) else ret
|
||||||
|
|
||||||
|
|
||||||
|
def current_user() -> Optional[User]:
|
||||||
|
"""
|
||||||
|
Returns the current user if authenticated.
|
||||||
|
"""
|
||||||
|
ret = get_current_user_or_auth_status(request)
|
||||||
|
return ret if isinstance(ret, User) else None
|
||||||
|
|
|
@ -1,21 +1,76 @@
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
StatusValue = namedtuple('StatusValue', ['code', 'message'])
|
from platypush.user import AuthenticationStatus
|
||||||
|
|
||||||
|
StatusValue = namedtuple('StatusValue', ['code', 'error', 'message'])
|
||||||
|
|
||||||
|
|
||||||
class AuthStatus(Enum):
|
class UserAuthStatus(Enum):
|
||||||
"""
|
"""
|
||||||
Models the status of the authentication.
|
Models the status of the authentication.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
OK = StatusValue(200, 'OK')
|
OK = StatusValue(200, AuthenticationStatus.OK, 'OK')
|
||||||
UNAUTHORIZED = StatusValue(401, 'Unauthorized')
|
INVALID_AUTH_TYPE = StatusValue(
|
||||||
NO_USERS = StatusValue(412, 'Please create a user first')
|
400, AuthenticationStatus.INVALID_AUTH_TYPE, 'Invalid authentication type'
|
||||||
|
)
|
||||||
|
INVALID_CREDENTIALS = StatusValue(
|
||||||
|
401, AuthenticationStatus.INVALID_CREDENTIALS, 'Invalid credentials'
|
||||||
|
)
|
||||||
|
INVALID_JWT_TOKEN = StatusValue(
|
||||||
|
401, AuthenticationStatus.INVALID_JWT_TOKEN, 'Invalid JWT token'
|
||||||
|
)
|
||||||
|
INVALID_OTP_CODE = StatusValue(
|
||||||
|
401, AuthenticationStatus.INVALID_OTP_CODE, 'Invalid OTP code'
|
||||||
|
)
|
||||||
|
INVALID_METHOD = StatusValue(
|
||||||
|
405, AuthenticationStatus.INVALID_METHOD, 'Invalid method'
|
||||||
|
)
|
||||||
|
MISSING_OTP_CODE = StatusValue(
|
||||||
|
401, AuthenticationStatus.MISSING_OTP_CODE, 'Missing OTP code'
|
||||||
|
)
|
||||||
|
MISSING_PASSWORD = StatusValue(
|
||||||
|
400, AuthenticationStatus.MISSING_PASSWORD, 'Missing password'
|
||||||
|
)
|
||||||
|
INVALID_SESSION = StatusValue(
|
||||||
|
401, AuthenticationStatus.INVALID_CREDENTIALS, 'Invalid session'
|
||||||
|
)
|
||||||
|
INVALID_TOKEN = StatusValue(
|
||||||
|
400, AuthenticationStatus.INVALID_JWT_TOKEN, 'Invalid token'
|
||||||
|
)
|
||||||
|
MISSING_USERNAME = StatusValue(
|
||||||
|
400, AuthenticationStatus.MISSING_USERNAME, 'Missing username'
|
||||||
|
)
|
||||||
|
PASSWORD_MISMATCH = StatusValue(
|
||||||
|
400, AuthenticationStatus.PASSWORD_MISMATCH, 'Password mismatch'
|
||||||
|
)
|
||||||
|
REGISTRATION_DISABLED = StatusValue(
|
||||||
|
401, AuthenticationStatus.REGISTRATION_DISABLED, 'Registrations are disabled'
|
||||||
|
)
|
||||||
|
REGISTRATION_REQUIRED = StatusValue(
|
||||||
|
412, AuthenticationStatus.REGISTRATION_REQUIRED, 'Please create a user first'
|
||||||
|
)
|
||||||
|
UNKNOWN_ERROR = StatusValue(
|
||||||
|
500, AuthenticationStatus.UNKNOWN_ERROR, 'Unknown error'
|
||||||
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
'code': self.value[0],
|
'code': self.value[0],
|
||||||
'message': self.value[1],
|
'error': self.value[1].name,
|
||||||
|
'message': self.value[2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_response(self):
|
||||||
|
return jsonify(self.to_dict()), self.value[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def by_status(status: AuthenticationStatus):
|
||||||
|
for auth_status in UserAuthStatus:
|
||||||
|
if auth_status.value[1] == status:
|
||||||
|
return auth_status
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -1,24 +1,57 @@
|
||||||
|
from multiprocessing import Lock
|
||||||
|
|
||||||
from platypush.bus.redis import RedisBus
|
from platypush.bus.redis import RedisBus
|
||||||
|
from platypush.context import get_bus
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.context import get_backend
|
|
||||||
from platypush.message import Message
|
from platypush.message import Message
|
||||||
from platypush.message.request import Request
|
from platypush.message.request import Request
|
||||||
from platypush.utils import get_redis_conf, get_message_response
|
from platypush.utils import get_message_response
|
||||||
|
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
_bus = None
|
|
||||||
|
class BusWrapper: # pylint: disable=too-few-public-methods
|
||||||
|
"""
|
||||||
|
Lazy singleton wrapper for the bus object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._redis_queue = None
|
||||||
|
self._bus = None
|
||||||
|
self._bus_lock = Lock()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bus(self) -> RedisBus:
|
||||||
|
"""
|
||||||
|
Lazy getter/initializer for the bus object.
|
||||||
|
"""
|
||||||
|
with self._bus_lock:
|
||||||
|
if not self._bus:
|
||||||
|
self._bus = get_bus()
|
||||||
|
|
||||||
|
bus_: RedisBus = self._bus # type: ignore
|
||||||
|
return bus_
|
||||||
|
|
||||||
|
def post(self, msg):
|
||||||
|
"""
|
||||||
|
Send a message to the bus.
|
||||||
|
|
||||||
|
:param msg: The message to send.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.bus.post(msg)
|
||||||
|
except Exception as e:
|
||||||
|
logger().exception(e)
|
||||||
|
|
||||||
|
|
||||||
|
_bus = BusWrapper()
|
||||||
|
|
||||||
|
|
||||||
def bus():
|
def bus():
|
||||||
"""
|
"""
|
||||||
Lazy getter/initializer for the bus object.
|
Lazy getter/initializer for the bus object.
|
||||||
"""
|
"""
|
||||||
global _bus # pylint: disable=global-statement
|
return _bus.bus
|
||||||
if _bus is None:
|
|
||||||
redis_queue = get_backend('http').bus.redis_queue # type: ignore
|
|
||||||
_bus = RedisBus(**get_redis_conf(), redis_queue=redis_queue)
|
|
||||||
return _bus
|
|
||||||
|
|
||||||
|
|
||||||
def send_message(msg, wait_for_response=True):
|
def send_message(msg, wait_for_response=True):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from threading import Thread
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
from tornado.websocket import WebSocketHandler
|
from tornado.websocket import WebSocketHandler
|
||||||
|
|
||||||
from platypush.backend.http.app.utils.auth import AuthStatus, get_auth_status
|
from platypush.backend.http.app.utils.auth import UserAuthStatus, get_auth_status
|
||||||
|
|
||||||
from ..mixins import MessageType, PubSubMixin
|
from ..mixins import MessageType, PubSubMixin
|
||||||
|
|
||||||
|
@ -25,9 +25,9 @@ class WSRoute(WebSocketHandler, Thread, PubSubMixin, ABC):
|
||||||
|
|
||||||
def open(self, *_, **__):
|
def open(self, *_, **__):
|
||||||
auth_status = get_auth_status(self.request)
|
auth_status = get_auth_status(self.request)
|
||||||
if auth_status != AuthStatus.OK:
|
if auth_status != UserAuthStatus.OK:
|
||||||
self.close(code=1008, reason=auth_status.value.message) # Policy Violation
|
self.close(code=1008, reason=auth_status.value.message) # Policy Violation
|
||||||
return
|
raise ValueError(f'Unauthorized connection: {auth_status.value.message}')
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Client %s connected to %s', self.request.remote_ip, self.request.path
|
'Client %s connected to %s', self.request.remote_ip, self.request.path
|
||||||
|
|
10
platypush/backend/http/manifest.json
Normal file
10
platypush/backend/http/manifest.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"manifest": {
|
||||||
|
"events": {},
|
||||||
|
"install": {
|
||||||
|
"pip": []
|
||||||
|
},
|
||||||
|
"package": "platypush.backend.http",
|
||||||
|
"type": "backend"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
manifest:
|
|
||||||
events: {}
|
|
||||||
install:
|
|
||||||
pip: []
|
|
||||||
package: platypush.backend.http
|
|
||||||
type: backend
|
|
|
@ -1,7 +1,6 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from typing import Generator, Optional
|
from typing import Generator, Optional
|
||||||
|
|
||||||
from platypush.message import JSONAble
|
from platypush.message import JSONAble
|
||||||
|
@ -57,9 +56,6 @@ class MediaHandler(JSONAble, ABC):
|
||||||
logging.exception(e)
|
logging.exception(e)
|
||||||
errors[hndl_class.__name__] = str(e)
|
errors[hndl_class.__name__] = str(e)
|
||||||
|
|
||||||
if os.path.exists(source):
|
|
||||||
source = f'file://{source}'
|
|
||||||
|
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
f'The source {source} has no handlers associated. Errors: {errors}'
|
f'The source {source} has no handlers associated. Errors: {errors}'
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,6 +15,9 @@ class FileHandler(MediaHandler):
|
||||||
prefix_handlers = ['file://']
|
prefix_handlers = ['file://']
|
||||||
|
|
||||||
def __init__(self, source, *args, **kwargs):
|
def __init__(self, source, *args, **kwargs):
|
||||||
|
if isinstance(source, str) and os.path.exists(source):
|
||||||
|
source = f'file://{source}'
|
||||||
|
|
||||||
super().__init__(source, *args, **kwargs)
|
super().__init__(source, *args, **kwargs)
|
||||||
|
|
||||||
self.path = os.path.abspath(
|
self.path = os.path.abspath(
|
||||||
|
@ -33,7 +36,7 @@ class FileHandler(MediaHandler):
|
||||||
), f'{source} is not a valid media file (detected format: {self.mime_type})'
|
), f'{source} is not a valid media file (detected format: {self.mime_type})'
|
||||||
|
|
||||||
self.extension = mimetypes.guess_extension(self.mime_type)
|
self.extension = mimetypes.guess_extension(self.mime_type)
|
||||||
if self.url and self.extension:
|
if self.url and self.extension and not self.url.endswith(self.extension):
|
||||||
self.url += self.extension
|
self.url += self.extension
|
||||||
self.content_length = os.path.getsize(self.path)
|
self.content_length = os.path.getsize(self.path)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import re
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
from platypush.backend.http.app import template_folder
|
from platypush.backend.http.app import template_folder
|
||||||
|
from platypush.backend.http.app.utils import current_user
|
||||||
|
|
||||||
|
|
||||||
class HttpUtils:
|
class HttpUtils:
|
||||||
|
@ -130,5 +131,9 @@ class HttpUtils:
|
||||||
path = path[0] if len(path) == 1 else os.path.join(*path)
|
path = path[0] if len(path) == 1 else os.path.join(*path)
|
||||||
return os.path.isfile(path)
|
return os.path.isfile(path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def current_user():
|
||||||
|
return current_user()
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" src="/static/js/chunk-vendors.aeea9c55.js"></script><script defer="defer" src="/static/js/app.e71ae2ab.js"></script><link href="/static/css/chunk-vendors.a2412607.css" rel="stylesheet"><link href="/static/css/app.5b1362a4.css" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#ffffff"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="Platypush"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#ffffff"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><!--[if IE]><link rel="icon" href="/favicon.ico"><![endif]--><link rel="stylesheet" href="/fonts/poppins.css"><title>platypush</title><script defer="defer" src="/static/js/chunk-vendors.83e191d2.js"></script><script defer="defer" src="/static/js/app.668abf05.js"></script><link href="/static/css/chunk-vendors.d510eff2.css" rel="stylesheet"><link href="/static/css/app.f97a4bca.css" rel="stylesheet"><link rel="icon" type="image/svg+xml" href="/img/icons/favicon.svg"><link rel="icon" type="image/png" sizes="32x32" href="/img/icons/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/icons/favicon-16x16.png"><link rel="manifest" href="/manifest.json"><meta name="theme-color" content="#ffffff"><meta name="apple-mobile-web-app-capable" content="no"><meta name="apple-mobile-web-app-status-bar-style" content="default"><meta name="apple-mobile-web-app-title" content="Platypush"><link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-152x152.png"><link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#ffffff"><meta name="msapplication-TileImage" content="/img/icons/msapplication-icon-144x144.png"><meta name="msapplication-TileColor" content="#000000"></head><body><noscript><strong>We're sorry but platypush doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/1008.80353e6d.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/1008.80353e6d.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/1131.f2c6f156.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/1131.f2c6f156.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
platypush/backend/http/webapp/dist/static/css/1146.58e6489b.css
vendored
Normal file
1
platypush/backend/http/webapp/dist/static/css/1146.58e6489b.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue