Compare commits
1216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c215b72e | ||
|
|
ea0598e507 | ||
|
|
28c3d9d2e4 | ||
|
|
e9f9d9dc98 | ||
|
|
bb75bfd15d | ||
|
|
9c84fb5887 | ||
|
|
3bb9272f06 | ||
|
|
a735e4ff29 | ||
|
|
63948a6de0 | ||
|
|
a470d77938 | ||
|
|
833be688ac | ||
|
|
fc7ae0ec4e | ||
|
|
753f5fc517 | ||
|
|
f1b7ef303d | ||
|
|
e7d4b5051b | ||
|
|
b7b3aa1eb7 | ||
|
|
f083d6b53f | ||
|
|
7caa5c5d57 | ||
|
|
65c2722a20 | ||
|
|
6b3fc3d492 | ||
|
|
fec9776def | ||
|
|
bfeab3648c | ||
|
|
c0f2409fcc | ||
|
|
ef5d89f323 | ||
|
|
9bcbffde5d | ||
|
|
c37735f2e8 | ||
|
|
165abc7bea | ||
|
|
7aaafb90e3 | ||
|
|
f07c60afb0 | ||
|
|
6adbba54ce | ||
|
|
97db4d714a | ||
|
|
12ce669566 | ||
|
|
4496e1d509 | ||
|
|
3b3f37365a | ||
|
|
22c91be127 | ||
|
|
3ec3e9672e | ||
|
|
86daa70ccb | ||
|
|
db97c3b2d4 | ||
|
|
4f298bbc8c | ||
|
|
8113f794ab | ||
|
|
14c18bd668 | ||
|
|
f779f0345e | ||
|
|
ebacfd43be | ||
|
|
e4a7172517 | ||
|
|
3747eaa3a7 | ||
|
|
761d8d1c03 | ||
|
|
4e7f720214 | ||
|
|
757c3a8aed | ||
|
|
87b0ae6614 | ||
|
|
920161b920 | ||
|
|
e7f7dcbb78 | ||
|
|
cc4a97db28 | ||
|
|
b546aeb440 | ||
|
|
99679a800d | ||
|
|
7b9b0d8a84 | ||
|
|
8e153cd92f | ||
|
|
d509abdd5c | ||
|
|
96c51af15a | ||
|
|
68004e1d34 | ||
|
|
fcedea110d | ||
|
|
68aedf07ae | ||
|
|
094f7cea94 | ||
|
|
765a749959 | ||
|
|
cf7983ca11 | ||
|
|
609039baeb | ||
|
|
03f1a3dbc0 | ||
|
|
75dc9d4d1d | ||
|
|
5beeeb958b | ||
|
|
a22f032924 | ||
|
|
3e034c85d6 | ||
|
|
d3c5feaf1b | ||
|
|
96c62f556b | ||
|
|
ebdad3f7c7 | ||
|
|
2fc2f1ddb3 | ||
|
|
a1af6e3892 | ||
|
|
726acb9c29 | ||
|
|
54fde33a20 | ||
|
|
b8cc75c6b4 | ||
|
|
b13fe7f3e4 | ||
|
|
81372d6a6b | ||
|
|
918f8816c5 | ||
|
|
bf981935cb | ||
|
|
1fa92f78e4 | ||
|
|
07564bbde3 | ||
|
|
4014e93155 | ||
|
|
f81224a2a6 | ||
|
|
8760152159 | ||
|
|
5694f30a94 | ||
|
|
156478b381 | ||
|
|
ad416b9cb2 | ||
|
|
2e39a5e573 | ||
|
|
cab099d77f | ||
|
|
0b5e93fd60 | ||
|
|
6e2ba78204 | ||
|
|
115f5ae6a3 | ||
|
|
bf12016315 | ||
|
|
b544931ee5 | ||
|
|
9cef626b28 | ||
|
|
708d382a3f | ||
|
|
f24ea4a5f8 | ||
|
|
6ddd09ff1f | ||
|
|
ddc560e862 | ||
|
|
6f452c62de | ||
|
|
76bb95098c | ||
|
|
0e241f56fb | ||
|
|
8ac3bb9711 | ||
|
|
ff62f8821a | ||
|
|
90c433443f | ||
|
|
8a37663c89 | ||
|
|
bc4015ac50 | ||
|
|
f13c0d78a8 | ||
|
|
cc3871adf6 | ||
|
|
3e52beef14 | ||
|
|
48403ce940 | ||
|
|
6564df8082 | ||
|
|
73202e1483 | ||
|
|
dc60e97415 | ||
|
|
ad40d7d8a9 | ||
|
|
f88f71d933 | ||
|
|
d688dd02c8 | ||
|
|
456c99d7db | ||
|
|
e4f03fac4b | ||
|
|
2cb72e1f48 | ||
|
|
d800b97f69 | ||
|
|
0674e04ee1 | ||
|
|
d56f321aad | ||
|
|
bedd2bbb23 | ||
|
|
27ef7ce560 | ||
|
|
775ebd3b1e | ||
|
|
49c7d83840 | ||
|
|
a30469f6ec | ||
|
|
045f9ef827 | ||
|
|
abc13575c9 | ||
|
|
958b824dbc | ||
|
|
7b5f5abd22 | ||
|
|
5b7060c6a3 | ||
|
|
bbcba005c0 | ||
|
|
11d8b90f88 | ||
|
|
b531ec9b50 | ||
|
|
79790303a9 | ||
|
|
11c45a67b3 | ||
|
|
413ea07cbd | ||
|
|
890148051f | ||
|
|
3ba5d1f3fd | ||
|
|
ef5a0c3f75 | ||
|
|
0323b2783b | ||
|
|
b1e8bc4a46 | ||
|
|
e3a33d102e | ||
|
|
b9a3ed1d74 | ||
|
|
cb0d4e8bd7 | ||
|
|
5ee7bdc55e | ||
|
|
35ed4e20f0 | ||
|
|
a5faf0699a | ||
|
|
cb249b30af | ||
|
|
bb1e3a2c72 | ||
|
|
540b8d7c13 | ||
|
|
99e524589c | ||
|
|
295343be85 | ||
|
|
6f61d2246d | ||
|
|
e0cd4ed379 | ||
|
|
9f3269bce7 | ||
|
|
464fabc3bb | ||
|
|
c187b948d9 | ||
|
|
eb85ee4d35 | ||
|
|
de6bc02ad8 | ||
|
|
d355b3167f | ||
|
|
6cd846dfd8 | ||
|
|
0c102f5324 | ||
|
|
f50596c4a1 | ||
|
|
5d289ce023 | ||
|
|
2722e8482d | ||
|
|
895dcf5a30 | ||
|
|
ac25c9cd7f | ||
|
|
47d00d1f27 | ||
|
|
6bab805528 | ||
|
|
6efd28d904 | ||
|
|
04329bf171 | ||
|
|
3d56b6864e | ||
|
|
e2e675e469 | ||
|
|
aceb98b4a0 | ||
|
|
b848faa2c0 | ||
|
|
ea04f5391e | ||
|
|
58e61e514a | ||
|
|
b91918b04d | ||
|
|
8032fa0bcc | ||
|
|
1f0c641610 | ||
|
|
37fa9345cf | ||
|
|
2c31032a1c | ||
|
|
aeb85486c4 | ||
|
|
4f5fe6723b | ||
|
|
53a8e6df51 | ||
|
|
f45409e456 | ||
|
|
34df600350 | ||
|
|
255640a385 | ||
|
|
442bcf7e4f | ||
|
|
3a8540a439 | ||
|
|
681038cbd4 | ||
|
|
bb8c450452 | ||
|
|
5e41de8edd | ||
|
|
47f7987210 | ||
|
|
3515aee8e8 | ||
|
|
23223f3925 | ||
|
|
f049973349 | ||
|
|
2cdef91d11 | ||
|
|
297ec33e8e | ||
|
|
dc55959df4 | ||
|
|
311b64acd1 | ||
|
|
89f11ab630 | ||
|
|
9c68a7970d | ||
|
|
18d619efa1 | ||
|
|
6490c67a6c | ||
|
|
8cdf87d72b | ||
|
|
46da6d0ddc | ||
|
|
89b9f0a4f9 | ||
|
|
887f1f7c71 | ||
|
|
c1f7b665d5 | ||
|
|
26fc6b7056 | ||
|
|
3d45db2606 | ||
|
|
91603945ef | ||
|
|
d6df3b980c | ||
|
|
d1185d0f5f | ||
|
|
f35132e182 | ||
|
|
09d22a9f2d | ||
|
|
b0ee05f07d | ||
|
|
bb33c11a6b | ||
|
|
728152a31c | ||
|
|
048f4bdf90 | ||
|
|
8c405b251f | ||
|
|
53ba09a2fe | ||
|
|
0d62c5ecfa | ||
|
|
44bb1e6803 | ||
|
|
6f69f3b8f5 | ||
|
|
d97576678d | ||
|
|
88bf4f9903 | ||
|
|
f07227e560 | ||
|
|
b197c678ef | ||
|
|
d13981b489 | ||
|
|
90d4681ae8 | ||
|
|
ce228630ce | ||
|
|
855fdee332 | ||
|
|
f8745636f2 | ||
|
|
aa07ff1682 | ||
|
|
4c3e310634 | ||
|
|
8b52330304 | ||
|
|
200dc1c91a | ||
|
|
531d4aaefc | ||
|
|
f8d98fb66f | ||
|
|
5b2472853b | ||
|
|
2dc234a94d | ||
|
|
fdb43b2c53 | ||
|
|
2161d1aa2f | ||
|
|
08c8534d39 | ||
|
|
9e7914e0b6 | ||
|
|
daebfec0a2 | ||
|
|
e81411cb9b | ||
|
|
6cdae16752 | ||
|
|
65a0c6cb23 | ||
|
|
36d0550bf9 | ||
|
|
92aee997da | ||
|
|
63e6f3a3fa | ||
|
|
bfafbad9dc | ||
|
|
4435fead5a | ||
|
|
e73715f30e | ||
|
|
f31e9c0d81 | ||
|
|
2d1a4737db | ||
|
|
91f89793da | ||
|
|
d201644a5b | ||
|
|
982dd001ef | ||
|
|
80bd4d134e | ||
|
|
92fda348cd | ||
|
|
ef3fdb7555 | ||
|
|
9eb30ffab3 | ||
|
|
0bf6c25a14 | ||
|
|
5bb7a7d30c | ||
|
|
85c9319dfd | ||
|
|
3fea051691 | ||
|
|
7826fdffeb | ||
|
|
1105605370 | ||
|
|
3de3dd426b | ||
|
|
0c7187d53f | ||
|
|
12db53d1eb | ||
|
|
cebde9d4c0 | ||
|
|
9395165916 | ||
|
|
a8daa2c77e | ||
|
|
49c873c858 | ||
|
|
c6fc5765f3 | ||
|
|
62cbbf57e7 | ||
|
|
b81c5636cc | ||
|
|
d867649a93 | ||
|
|
cd08259012 | ||
|
|
e814af1af5 | ||
|
|
ecbff16a88 | ||
|
|
baffa4a38c | ||
|
|
fad507d2dd | ||
|
|
053ee8284d | ||
|
|
4f05fa9375 | ||
|
|
7ecf1bcf94 | ||
|
|
f030cdcb02 | ||
|
|
7474a1868e | ||
|
|
9a4d90790a | ||
|
|
7e38ee07c7 | ||
|
|
b9574e2d67 | ||
|
|
6431613363 | ||
|
|
a00392166c | ||
|
|
bfcad6c5f2 | ||
|
|
7daf2162ef | ||
|
|
dec8d75083 | ||
|
|
f486f8de1d | ||
|
|
93b1e9c371 | ||
|
|
6440f57467 | ||
|
|
25451eb763 | ||
|
|
dbefb80f63 | ||
|
|
889d68dc7b | ||
|
|
42dbc04ff9 | ||
|
|
82c8ef1e4b | ||
|
|
4deb45df3c | ||
|
|
4b02960fd1 | ||
|
|
15e5564b12 | ||
|
|
e66241ddcb | ||
|
|
d3990a6c55 | ||
|
|
be1d081629 | ||
|
|
fafb524a47 | ||
|
|
da1b9ccac7 | ||
|
|
7b97e1ca26 | ||
|
|
7f11549337 | ||
|
|
987e0ddd4e | ||
|
|
8fd097836f | ||
|
|
5acee68987 | ||
|
|
25a1ca5a9f | ||
|
|
32af107699 | ||
|
|
b929e16f2c | ||
|
|
1c942186aa | ||
|
|
d9f8785372 | ||
|
|
8758d74e32 | ||
|
|
6448a7db9e | ||
|
|
46d1da7cd3 | ||
|
|
77c05a4d4f | ||
|
|
4024334c0c | ||
|
|
3294b27029 | ||
|
|
0d4747e8e9 | ||
|
|
1ebc648158 | ||
|
|
7e5bfd4b10 | ||
|
|
5074f4d7af | ||
|
|
22feec49cb | ||
|
|
4b59876d8b | ||
|
|
3c278bc930 | ||
|
|
0221d29ecb | ||
|
|
130b56f02d | ||
|
|
e86f5f4c3c | ||
|
|
3b0701e772 | ||
|
|
9874dce520 | ||
|
|
2f50ab36fd | ||
|
|
6124b9b3f3 | ||
|
|
7ff492df6c | ||
|
|
a8ce35c13f | ||
|
|
26d9864051 | ||
|
|
a3a22d353c | ||
|
|
dd5eecf9f9 | ||
|
|
7e0e0b0520 | ||
|
|
8bee09cd01 | ||
|
|
deb117fc34 | ||
|
|
a9a0005007 | ||
|
|
4eb7afead6 | ||
|
|
d1b5b74060 | ||
|
|
cf91ee62ed | ||
|
|
277690ef79 | ||
|
|
f7f3530a33 | ||
|
|
2d3a5c739c | ||
|
|
3dbb993d35 | ||
|
|
508168b49e | ||
|
|
0e1cbd7e7b | ||
|
|
e73ecb7a52 | ||
|
|
62be8adc65 | ||
|
|
acc8892f26 | ||
|
|
51b59ae103 | ||
|
|
8888807780 | ||
|
|
0f0355fd01 | ||
|
|
d19f7d6b53 | ||
|
|
a31f174375 | ||
|
|
18ae03554f | ||
|
|
07de4e5015 | ||
|
|
57e6469564 | ||
|
|
cd2c37057d | ||
|
|
a35ca762e3 | ||
|
|
fd10b2600f | ||
|
|
1384091d95 | ||
|
|
ca29ea2d46 | ||
|
|
d8c9ae4ff6 | ||
|
|
4403ea8e18 | ||
|
|
528829ffda | ||
|
|
84429a3399 | ||
|
|
6be5d6cbcb | ||
|
|
c59ea2000c | ||
|
|
30ee554f56 | ||
|
|
c1d984b86d | ||
|
|
fe1570d0bc | ||
|
|
edfd295fb4 | ||
|
|
d57d33b620 | ||
|
|
aedea1bea6 | ||
|
|
535a100314 | ||
|
|
360c25d084 | ||
|
|
d47afe05f4 | ||
|
|
942792cdfa | ||
|
|
685254950e | ||
|
|
e6cc7fce1a | ||
|
|
d8b1f03ac4 | ||
|
|
d81679fbae | ||
|
|
ebb49fce97 | ||
|
|
0fd4f516b1 | ||
|
|
9fff5781f4 | ||
|
|
e19352a69f | ||
|
|
09b96e5983 | ||
|
|
bd9f4258e2 | ||
|
|
a37cdf43f3 | ||
|
|
4821b30634 | ||
|
|
dd8dfcb2b1 | ||
|
|
73c7f22bd1 | ||
|
|
e7ca335d83 | ||
|
|
3730775018 | ||
|
|
4fcba32f74 | ||
|
|
b39ad5c688 | ||
|
|
a41b382dba | ||
|
|
9092f42834 | ||
|
|
af563aa6e5 | ||
|
|
f37edcb751 | ||
|
|
5d33dcf68e | ||
|
|
947da02b3c | ||
|
|
838d108d25 | ||
|
|
4a19af3353 | ||
|
|
4da1c8c2b6 | ||
|
|
c988239fa8 | ||
|
|
94e3c13b3e | ||
|
|
36f3860c4c | ||
|
|
f78fa28822 | ||
|
|
2de7182c55 | ||
|
|
f3e1606440 | ||
|
|
b7236319ec | ||
|
|
556c31d4ea | ||
|
|
0bf8cd65cd | ||
|
|
4d27f7fc7a | ||
|
|
a4f59203b0 | ||
|
|
eeb9b07bce | ||
|
|
9ae16163bb | ||
|
|
c5ce66bd4d | ||
|
|
da8dd7def8 | ||
|
|
a4b5d6dea8 | ||
|
|
77799b2917 | ||
|
|
5ff3839239 | ||
|
|
d560df5b1e | ||
|
|
91c8ce8089 | ||
|
|
0fe72b41bf | ||
|
|
a1d93cd6af | ||
|
|
53ac01eda4 | ||
|
|
4cea755065 | ||
|
|
be4e83d69c | ||
|
|
1e58a33c68 | ||
|
|
15e1766920 | ||
|
|
a220ba8dfb | ||
|
|
b29c24a405 | ||
|
|
fbe3553b25 | ||
|
|
f727e2c5b2 | ||
|
|
6c6af623a6 | ||
|
|
548dceda28 | ||
|
|
e67b2e91fb | ||
|
|
412fe31da6 | ||
|
|
1bfec54c93 | ||
|
|
5b319d6612 | ||
|
|
626d623841 | ||
|
|
08343298fb | ||
|
|
7a343bbc08 | ||
|
|
2be34ed063 | ||
|
|
7e32c8c4f0 | ||
|
|
a0871e7c47 | ||
|
|
f6834ee63d | ||
|
|
54b8e30cff | ||
|
|
504d3e39de | ||
|
|
4e2dce7f18 | ||
|
|
b31506d94e | ||
|
|
5961bc6086 | ||
|
|
b0ffa457df | ||
|
|
9e3fa0ea9a | ||
|
|
ff69d0ba6d | ||
|
|
cf25472746 | ||
|
|
5e651d7b08 | ||
|
|
bcd1011b68 | ||
|
|
f8f0715a3b | ||
|
|
e27e08bfd7 | ||
|
|
ae4d47af43 | ||
|
|
b32271e375 | ||
|
|
753e233e27 | ||
|
|
d6669bfa70 | ||
|
|
94b288671d | ||
|
|
a444e53a99 | ||
|
|
792846f727 | ||
|
|
6b9fa7bf8a | ||
|
|
c4378d19db | ||
|
|
bdebd97dc0 | ||
|
|
cf2bd6c095 | ||
|
|
08f3675f71 | ||
|
|
8d53ddcf93 | ||
|
|
c20dc24ccf | ||
|
|
b60be0db6d | ||
|
|
756e7345cf | ||
|
|
eb3489b34f | ||
|
|
9693ce3dcd | ||
|
|
6e88c1f4fc | ||
|
|
67c60cb677 | ||
|
|
1a6b0d2b6e | ||
|
|
5aaab7904f | ||
|
|
9885c25a2e | ||
|
|
27ad7a4cf7 | ||
|
|
6551eeb938 | ||
|
|
36c23c1e4f | ||
|
|
6b4d4da455 | ||
|
|
aa2891fc87 | ||
|
|
db526fc611 | ||
|
|
a869acd5dc | ||
|
|
f763cfb53a | ||
|
|
25a8d3807e | ||
|
|
b10b558358 | ||
|
|
504b602c0b | ||
|
|
f04411e137 | ||
|
|
1336a87ae2 | ||
|
|
872c366384 | ||
|
|
762d5325fb | ||
|
|
7f37633423 | ||
|
|
8ec4031ba3 | ||
|
|
4c10996c09 | ||
|
|
4d3acb2c4c | ||
|
|
833d02b032 | ||
|
|
30198fab87 | ||
|
|
51768958c6 | ||
|
|
3e55cd1e31 | ||
|
|
35f0fead53 | ||
|
|
a95d8bff29 | ||
|
|
48332a4ffa | ||
|
|
2266bbc320 | ||
|
|
b682685a3b | ||
|
|
91411437e2 | ||
|
|
119bed7024 | ||
|
|
6d70a5b24b | ||
|
|
a99ee04aca | ||
|
|
3ca2315290 | ||
|
|
d4bcf229e9 | ||
|
|
3950455a3f | ||
|
|
7e8e242db0 | ||
|
|
cda90f20af | ||
|
|
8ba393ebc0 | ||
|
|
2de1570a98 | ||
|
|
6b01e0d44d | ||
|
|
af4dcd1e2a | ||
|
|
a8ce68959d | ||
|
|
05bc38565c | ||
|
|
574ca4734d | ||
|
|
0957dd58c2 | ||
|
|
4db5d96bb1 | ||
|
|
76c19731cb | ||
|
|
fea368aaae | ||
|
|
1f8bc027c8 | ||
|
|
f2240ebf0d | ||
|
|
9b9f34ae96 | ||
|
|
86559d5c76 | ||
|
|
40ec5b9933 | ||
|
|
fbb9f20026 | ||
|
|
d5a33cf242 | ||
|
|
c35fdc2cbe | ||
|
|
84d5bc8f67 | ||
|
|
b8d9d22545 | ||
|
|
788afa1025 | ||
|
|
6ca3ab899c | ||
|
|
d4096d0062 | ||
|
|
306ede47d6 | ||
|
|
fc0e86ffd8 | ||
|
|
729fc7baf7 | ||
|
|
2d83e9ff7e | ||
|
|
a0af76364a | ||
|
|
169622bf95 | ||
|
|
78b5136b9a | ||
|
|
e546f50141 | ||
|
|
e35fe8d425 | ||
|
|
6ed2f7aaa6 | ||
|
|
084e8ec432 | ||
|
|
fd7f74682b | ||
|
|
9950c158a1 | ||
|
|
21125033ff | ||
|
|
1dc0b2234a | ||
|
|
0ea5c7fdc0 | ||
|
|
b538922c05 | ||
|
|
f0f4e8118e | ||
|
|
2f501697db | ||
|
|
0a71d5b216 | ||
|
|
0014db44f0 | ||
|
|
885d2ebf0f | ||
|
|
6d089a9818 | ||
|
|
de81c7e29f | ||
|
|
e49996c401 | ||
|
|
aa40a72075 | ||
|
|
19b7341e80 | ||
|
|
73645a7569 | ||
|
|
a9dac8c04c | ||
|
|
43fbbbcd16 | ||
|
|
fc57a8c97f | ||
|
|
1fb2ef5675 | ||
|
|
e0d994c35c | ||
|
|
cab30eb628 | ||
|
|
71df011556 | ||
|
|
b2828110e3 | ||
|
|
50eb05776a | ||
|
|
19715f25f6 | ||
|
|
d41a281d53 | ||
|
|
a8229631bd | ||
|
|
0a2cf6132f | ||
|
|
d7ab01063a | ||
|
|
6fb8f1ed7f | ||
|
|
a9b11012bc | ||
|
|
e7cb1f516b | ||
|
|
555d5abf59 | ||
|
|
93937ec3b5 | ||
|
|
93c7a6e31b | ||
|
|
a9cabe3d74 | ||
|
|
d6fd1d6894 | ||
|
|
375022ba95 | ||
|
|
75fdf6ec3d | ||
|
|
561c461a18 | ||
|
|
59ebf52fe2 | ||
|
|
89fb3fa619 | ||
|
|
9bd6abadf4 | ||
|
|
953a66ec47 | ||
|
|
4e826f4167 | ||
|
|
e97b90d4d7 | ||
|
|
fb6256d1ed | ||
|
|
7035a3fe9c | ||
|
|
62c29d55cc | ||
|
|
a83dbcf3ab | ||
|
|
e48bdcc45b | ||
|
|
0b473ef01f | ||
|
|
e03525a1d1 | ||
|
|
087172c79e | ||
|
|
8fd919bf04 | ||
|
|
2ad84db482 | ||
|
|
85536ff79e | ||
|
|
8b62c91d13 | ||
|
|
e7d1693517 | ||
|
|
e78b4882b3 | ||
|
|
e01144950b | ||
|
|
86ef665b12 | ||
|
|
f419a57e6d | ||
|
|
d7e8ec95de | ||
|
|
5a9bc1c66f | ||
|
|
1f9af8df89 | ||
|
|
0676b6c41f | ||
|
|
ac842e6273 | ||
|
|
ce8cdced4d | ||
|
|
b8e3fc636c | ||
|
|
519a5615cc | ||
|
|
168b217553 | ||
|
|
7d698d63e3 | ||
|
|
035dbde819 | ||
|
|
c373d8b2d6 | ||
|
|
8698c3c6a4 | ||
|
|
0edd2ba68b | ||
|
|
b91f0b5a18 | ||
|
|
24fa841c0d | ||
|
|
44558b8109 | ||
|
|
478b40d0ff | ||
|
|
8b816dc725 | ||
|
|
81a58f628b | ||
|
|
e98c9b46f1 | ||
|
|
b3ce7acfcb | ||
|
|
9fac79b1f0 | ||
|
|
591e3c5ca1 | ||
|
|
35d407afef | ||
|
|
a6447165b7 | ||
|
|
23800bb892 | ||
|
|
b47cb91f55 | ||
|
|
2d9e3fbc1d | ||
|
|
bf67e27737 | ||
|
|
3427c97e3e | ||
|
|
81e69a7166 | ||
|
|
564098b9d8 | ||
|
|
ec659174fb | ||
|
|
1a42d8280c | ||
|
|
b14f10d79d | ||
|
|
ee8facd1bf | ||
|
|
811657b553 | ||
|
|
95936f7c29 | ||
|
|
613d4cd9af | ||
|
|
7beb3d9974 | ||
|
|
6f2bb7f0b5 | ||
|
|
315b5fda91 | ||
|
|
a6aa89e502 | ||
|
|
3bf722c5fe | ||
|
|
e931c09a34 | ||
|
|
f8f5f35cc1 | ||
|
|
524941da0c | ||
|
|
22bba922f9 | ||
|
|
d928df7ab2 | ||
|
|
4b11bbe21f | ||
|
|
18bcd55972 | ||
|
|
057f306ed9 | ||
|
|
76bbb3f147 | ||
|
|
0f3ad8bb69 | ||
|
|
1d47b9074f | ||
|
|
5167fde080 | ||
|
|
a62648ee68 | ||
|
|
5dee414596 | ||
|
|
8cf9b1f905 | ||
|
|
6bf1920160 | ||
|
|
33f8070e57 | ||
|
|
4d2a018032 | ||
|
|
ca7fb540ee | ||
|
|
beb0712ce9 | ||
|
|
a081b14794 | ||
|
|
e416acf6bd | ||
|
|
bf94f76509 | ||
|
|
ac239a309c | ||
|
|
0f12586166 | ||
|
|
b1b50ce561 | ||
|
|
8e2bf48ab4 | ||
|
|
6ec5022a0d | ||
|
|
ef97e0ac76 | ||
|
|
30736a055d | ||
|
|
d0905a29be | ||
|
|
fe5cf69b7a | ||
|
|
c560ec0f9f | ||
|
|
71554e0c85 | ||
|
|
0efd7c5718 | ||
|
|
901ad7529e | ||
|
|
b64bcc9738 | ||
|
|
fddb7b7584 | ||
|
|
b91302ddf8 | ||
|
|
ea0293bd4e | ||
|
|
51f2f4cc6a | ||
|
|
2d93b3b7ee | ||
|
|
0f41d1e6cf | ||
|
|
36edd4ab0d | ||
|
|
716d6a931a | ||
|
|
72bf280e2d | ||
|
|
326c2cf70a | ||
|
|
2816c6277d | ||
|
|
99875b9176 | ||
|
|
0e21942cd6 | ||
|
|
b2b5083102 | ||
|
|
c0f316d049 | ||
|
|
2c6d08319b | ||
|
|
5d8f139356 | ||
|
|
87ef71b415 | ||
|
|
cf99ae880c | ||
|
|
8e86078394 | ||
|
|
beea903879 | ||
|
|
c5e4c5d509 | ||
|
|
fac951c733 | ||
|
|
83449f3332 | ||
|
|
2a9fc8c7a5 | ||
|
|
f8d4f79271 | ||
|
|
bc466d0c6f | ||
|
|
382a0f4c3c | ||
|
|
488c2f5df5 | ||
|
|
43effd0c32 | ||
|
|
af61549bf1 | ||
|
|
22a0d8925d | ||
|
|
59a014f681 | ||
|
|
9944cc2db9 | ||
|
|
570e3a1e54 | ||
|
|
a9bde40661 | ||
|
|
b03a185e88 | ||
|
|
e450587eea | ||
|
|
30a529baac | ||
|
|
adbb74f56b | ||
|
|
223b4df172 | ||
|
|
44dc315914 | ||
|
|
c959e2ce4d | ||
|
|
57b10dd514 | ||
|
|
9da0f89613 | ||
|
|
4104cb334e | ||
|
|
94067a1ec2 | ||
|
|
3e9da3baf7 | ||
|
|
6129305b2c | ||
|
|
7165eb1f59 | ||
|
|
a4820de423 | ||
|
|
0c09f3b05f | ||
|
|
269d67f071 | ||
|
|
bdc0c0ffa2 | ||
|
|
c00f5f4330 | ||
|
|
a2c344de83 | ||
|
|
886ae64feb | ||
|
|
90a2c1f2e7 | ||
|
|
d772e43e44 | ||
|
|
8fdab39b18 | ||
|
|
f7d2771263 | ||
|
|
e8b1cca9ca | ||
|
|
d4d7219801 | ||
|
|
3273607fc3 | ||
|
|
55e21f8be3 | ||
|
|
dafb439a7d | ||
|
|
ab94de2f95 | ||
|
|
3dc0df0ac2 | ||
|
|
d701c5f27d | ||
|
|
a8f71c83da | ||
|
|
7a3e0d60f9 | ||
|
|
2687af31ca | ||
|
|
d51a6abb02 | ||
|
|
374ffbf01f | ||
|
|
871bc9f396 | ||
|
|
66b7df7cde | ||
|
|
bc76770ca4 | ||
|
|
7196361cf6 | ||
|
|
3e73d16cce | ||
|
|
3f8414c70a | ||
|
|
6ec2186bdf | ||
|
|
6dd575b276 | ||
|
|
1a98946d71 | ||
|
|
8922549bdb | ||
|
|
173b49aeb7 | ||
|
|
eee6046465 | ||
|
|
b76011be4f | ||
|
|
3d93d79b0b | ||
|
|
7dcc9b20a1 | ||
|
|
754b956206 | ||
|
|
47ac505cac | ||
|
|
e6e5231f63 | ||
|
|
78049d4a33 | ||
|
|
8a6cfe0b4d | ||
|
|
afedc78113 | ||
|
|
76b822213e | ||
|
|
ab3d5f3321 | ||
|
|
e1d42c8a87 | ||
|
|
f53c852a4d | ||
|
|
aaea889e47 | ||
|
|
bf98c74ecf | ||
|
|
fcadabd339 | ||
|
|
2a0edeb3c5 | ||
|
|
30f16e7207 | ||
|
|
dbe7e2e659 | ||
|
|
e16f05b130 | ||
|
|
07573a515a | ||
|
|
b3a2de50cf | ||
|
|
5388d3d4c0 | ||
|
|
c392d48174 | ||
|
|
967fab3411 | ||
|
|
d7845b78f6 | ||
|
|
a253858625 | ||
|
|
ad1aae16e3 | ||
|
|
9370913ace | ||
|
|
dcd2e234e8 | ||
|
|
762dac2581 | ||
|
|
1cf8d3037b | ||
|
|
40808bdcb9 | ||
|
|
2451d69341 | ||
|
|
e449853568 | ||
|
|
2082e960c2 | ||
|
|
7b2a083f98 | ||
|
|
270143a8f6 | ||
|
|
766b69d95e | ||
|
|
f5addc4947 | ||
|
|
55eb59c526 | ||
|
|
679cac4dbd | ||
|
|
a0a25d64f1 | ||
|
|
9875458b01 | ||
|
|
f0dccc58aa | ||
|
|
636bc22d52 | ||
|
|
fc6b6a9c6b | ||
|
|
1a6d78352c | ||
|
|
e351c35cc8 | ||
|
|
618cc32a17 | ||
|
|
a8bf670697 | ||
|
|
0bdf8ad6ce | ||
|
|
8f65e2e968 | ||
|
|
0d3f96c3a7 | ||
|
|
cfa7947020 | ||
|
|
b91de3f319 | ||
|
|
1704ae8cb1 | ||
|
|
50c6e6031d | ||
|
|
de92516d52 | ||
|
|
cd67d3e7ab | ||
|
|
c556878f11 | ||
|
|
3af4607171 | ||
|
|
111533fa2d | ||
|
|
5dc0a68b44 | ||
|
|
43e5bbbe21 | ||
|
|
42921f6a3e | ||
|
|
5892899114 | ||
|
|
4404c84e7f | ||
|
|
a86be55b5c | ||
|
|
5eea72a579 | ||
|
|
03247ddef8 | ||
|
|
e6e5b0f3cf | ||
|
|
9b977bafbf | ||
|
|
77f755e43c | ||
|
|
30bef15855 | ||
|
|
7bd8fadf76 | ||
|
|
21490faa9e | ||
|
|
f685582e1a | ||
|
|
f792166523 | ||
|
|
7c0754a70c | ||
|
|
2f33580f32 | ||
|
|
eb8f2777ae | ||
|
|
92332206f0 | ||
|
|
9787fce275 | ||
|
|
1c67b06c27 | ||
|
|
88eab75e30 | ||
|
|
6c5f776a7a | ||
|
|
ca0c56e748 | ||
|
|
e29e0ddb5b | ||
|
|
7ce75c271c | ||
|
|
884493e7aa | ||
|
|
bd05a4b35a | ||
|
|
fa7da1b23f | ||
|
|
1ec5d2ca3f | ||
|
|
1e9d184508 | ||
|
|
2934832a98 | ||
|
|
3635b6a367 | ||
|
|
2b97850eb2 | ||
|
|
c1d1b0e560 | ||
|
|
e1d9a00d67 | ||
|
|
35aa37e10e | ||
|
|
e38c470fb9 | ||
|
|
edd4584136 | ||
|
|
d7a84c1982 | ||
|
|
fe86b8a7d0 | ||
|
|
01f290b459 | ||
|
|
9a398e9291 | ||
|
|
1fbd11dbe8 | ||
|
|
68b26f8301 | ||
|
|
6877f3975e | ||
|
|
6a11ed5622 | ||
|
|
53bec00a7e | ||
|
|
c616ab324d | ||
|
|
7e21eb87db | ||
|
|
98cd33da05 | ||
|
|
d520694e12 | ||
|
|
3c4800efa8 | ||
|
|
bd227842d2 | ||
|
|
f47bf762ac | ||
|
|
1342208980 | ||
|
|
c8a9b15b4e | ||
|
|
b0bd6973d1 | ||
|
|
d10eb6d6bf | ||
|
|
0c5a332fa2 | ||
|
|
5a07e103c0 | ||
|
|
40fc5e9604 | ||
|
|
9799665951 | ||
|
|
b3fa667db1 | ||
|
|
027cf19d0f | ||
|
|
38119551d7 | ||
|
|
52d9cda61a | ||
|
|
f40fb9d3f7 | ||
|
|
9536ceaaa4 | ||
|
|
72beee1322 | ||
|
|
0ec822988d | ||
|
|
d1b1b90de3 | ||
|
|
058cac2e7b | ||
|
|
6ffdd4dad7 | ||
|
|
98d59ba4e0 | ||
|
|
938523c18b | ||
|
|
cc4e12c405 | ||
|
|
eb406ef951 | ||
|
|
5c87d109a3 | ||
|
|
3e020da66a | ||
|
|
78157f763f | ||
|
|
bcc0eeeb2f | ||
|
|
76b859f5bf | ||
|
|
676cf619d5 | ||
|
|
ce45bf2136 | ||
|
|
b25f786018 | ||
|
|
ca00796077 | ||
|
|
a1bbf13d6a | ||
|
|
76fa171575 | ||
|
|
ce30537ebd | ||
|
|
93b5b483cc | ||
|
|
27ef931670 | ||
|
|
fb727e75ec | ||
|
|
fa433c88a8 | ||
|
|
adbb5b9d38 | ||
|
|
cdc837e781 | ||
|
|
a92baa5d18 | ||
|
|
e913f25a47 | ||
|
|
9eb803388e | ||
|
|
eb81515f46 | ||
|
|
52461c0356 | ||
|
|
6b3800abf6 | ||
|
|
09fc81d7f4 | ||
|
|
82034a2586 | ||
|
|
5e001bed60 | ||
|
|
5d7972db56 | ||
|
|
403ad58274 | ||
|
|
a1a233e74f | ||
|
|
8dd72c95ab | ||
|
|
f794322392 | ||
|
|
afd52d1d37 | ||
|
|
ba7370171a | ||
|
|
deb364a8bd | ||
|
|
a3cf498e15 | ||
|
|
3b356d2d8c | ||
|
|
20e17b576a | ||
|
|
8e680ff576 | ||
|
|
29d26aeb15 | ||
|
|
33b7876826 | ||
|
|
0fc4b5eb22 | ||
|
|
e672f9f14c | ||
|
|
4d2e509950 | ||
|
|
a80e5c2aa9 | ||
|
|
cd375208ba | ||
|
|
316f482bf5 | ||
|
|
0f78390282 | ||
|
|
0b909fc02d | ||
|
|
d4c6561abd | ||
|
|
1f5e6537a5 | ||
|
|
97f2ae34ca | ||
|
|
d193afbeca | ||
|
|
abea430b6b | ||
|
|
00cec2b157 | ||
|
|
41ff0be839 | ||
|
|
25330533bd | ||
|
|
4afd1bd4b5 | ||
|
|
cf185efdfc | ||
|
|
4ec9756f58 | ||
|
|
cda7db5718 | ||
|
|
3b37fb5692 | ||
|
|
40c83803de | ||
|
|
a9811c164e | ||
|
|
8f000876b3 | ||
|
|
0af393236f | ||
|
|
3153c65f5a | ||
|
|
6ce825bd41 | ||
|
|
060f0efc16 | ||
|
|
c3fb00a307 | ||
|
|
988829a6db | ||
|
|
76935a300a | ||
|
|
a6a7710a79 | ||
|
|
873afb47cd | ||
|
|
ea99966057 | ||
|
|
aaed272bf2 | ||
|
|
e6775cd2d1 | ||
|
|
98a9e20cc0 | ||
|
|
ee37588959 | ||
|
|
cb12c6f441 | ||
|
|
72cf3e2240 | ||
|
|
815bdc35ac | ||
|
|
0330540f87 | ||
|
|
fefe2d82a4 | ||
|
|
1af8d1f77d | ||
|
|
4c653fea36 | ||
|
|
2ee0ed55f6 | ||
|
|
94981f4891 | ||
|
|
f72def0399 | ||
|
|
81fb0fc69f | ||
|
|
c3af0f4380 | ||
|
|
3a9e4950d4 | ||
|
|
06dada297b | ||
|
|
2b55a1873c | ||
|
|
c2e68bdc77 | ||
|
|
e1c3b312ff | ||
|
|
e235ed9fda | ||
|
|
5cda12dd3b | ||
|
|
a9d48083fd | ||
|
|
e28c50401e | ||
|
|
4a3b015a40 | ||
|
|
1a6727312c | ||
|
|
91d3d2596e | ||
|
|
192c9a4764 | ||
|
|
173c38563e | ||
|
|
d061721f56 | ||
|
|
218882b7c6 | ||
|
|
fed3ee4c4f | ||
|
|
8eed4b0127 | ||
|
|
c09ffb49e7 | ||
|
|
f331f4eb92 | ||
|
|
629b669c64 | ||
|
|
2dab900748 | ||
|
|
f864097f2e | ||
|
|
2c8be42bbc | ||
|
|
6691ae27f4 | ||
|
|
23fecb16b2 | ||
|
|
b037b08152 | ||
|
|
46fe3a7f5d | ||
|
|
61bd62403f | ||
|
|
5893d4b855 | ||
|
|
8016e6f211 | ||
|
|
a5560b04bd | ||
|
|
b9e171b1fd | ||
|
|
a633425baa | ||
|
|
e29e89c618 | ||
|
|
62c986161c | ||
|
|
6279c73402 | ||
|
|
22e103837f | ||
|
|
feba6e7bae | ||
|
|
95a6b48c3e | ||
|
|
90c6cee780 | ||
|
|
456ef556b1 | ||
|
|
ce98b2eb5a | ||
|
|
ee026714d4 | ||
|
|
736c39840f | ||
|
|
e755bc6b61 | ||
|
|
7ec9f2435c | ||
|
|
443d6fee52 | ||
|
|
b023616033 | ||
|
|
f182b88c58 | ||
|
|
93daadae4b | ||
|
|
fd1ec5d3fb | ||
|
|
b9a8a27807 | ||
|
|
27a36898a3 | ||
|
|
d8948c037b | ||
|
|
83f2749eab | ||
|
|
290435b5ba | ||
|
|
d09125c63c | ||
|
|
f0aa64373b | ||
|
|
2272883d5a | ||
|
|
2d0f6d89aa | ||
|
|
4cd1571c05 | ||
|
|
55be62bc3e | ||
|
|
da82f1c146 | ||
|
|
67f53d4112 | ||
|
|
88356281fb | ||
|
|
05198ea764 | ||
|
|
fe33d97d87 | ||
|
|
2c60dee48a | ||
|
|
8af9f9944a | ||
|
|
4e968d2338 | ||
|
|
7ba88977a4 | ||
|
|
0c5f6a68f9 | ||
|
|
bbd539278c | ||
|
|
be9d9ac6ff | ||
|
|
e8b37a5df8 | ||
|
|
d10d347e2b | ||
|
|
68689d74a0 | ||
|
|
4fc9bdb35b | ||
|
|
c0a05be44e | ||
|
|
482c9d5719 | ||
|
|
f063298bf7 | ||
|
|
bb1e454850 | ||
|
|
11770d90f1 | ||
|
|
8a415140b6 | ||
|
|
3dd83bffbf | ||
|
|
79987ffa22 | ||
|
|
764639bbba | ||
|
|
eb67116ee6 | ||
|
|
7baea9101e | ||
|
|
167fae9892 | ||
|
|
c7f5aa2e2b | ||
|
|
8c871bc5fa | ||
|
|
1f6bbc75ff | ||
|
|
23ae18d732 | ||
|
|
bf1e6230dc | ||
|
|
8af1c13d7e | ||
|
|
061945218a | ||
|
|
687edf2b0b | ||
|
|
1bf1e994fe | ||
|
|
7f91a27e4f | ||
|
|
f66510c74b | ||
|
|
e5de8b20ff | ||
|
|
dd96d71280 | ||
|
|
a687b2c438 | ||
|
|
ea262ca60b | ||
|
|
406fef6595 | ||
|
|
f7d8feac5d | ||
|
|
cd2ea2e579 | ||
|
|
b66654787c | ||
|
|
882a3467db | ||
|
|
27ac0bf43e | ||
|
|
4485622354 | ||
|
|
91b2b44768 | ||
|
|
8c276fa0a7 | ||
|
|
78b0e22091 | ||
|
|
96abbdf9a8 | ||
|
|
7c61392ff4 | ||
|
|
901abfb3e5 | ||
|
|
2eea836d9f | ||
|
|
9a08a6603a | ||
|
|
887126f5dd | ||
|
|
fad6a04a5f | ||
|
|
e834445b0b | ||
|
|
1aadd12006 | ||
|
|
26a1f30d32 | ||
|
|
e0a17c6a74 | ||
|
|
7ce1b5001c | ||
|
|
72a7759ca5 | ||
|
|
bf46c9f906 | ||
|
|
14bb85f301 | ||
|
|
e68dccbc17 | ||
|
|
ffc62574ec | ||
|
|
ca0889aaab | ||
|
|
772e12d11c | ||
|
|
7d04487b18 | ||
|
|
0b482116bb | ||
|
|
a579bcd463 | ||
|
|
ab7017ff12 | ||
|
|
3d5bea003a | ||
|
|
bc99dc34ee | ||
|
|
965c449f1c | ||
|
|
4679c6f355 | ||
|
|
a3351f4da8 | ||
|
|
422f13202b | ||
|
|
c470e40737 | ||
|
|
0f92ce2166 | ||
|
|
b1becb9ef5 | ||
|
|
3c1599b6b7 | ||
|
|
3e53b742f4 | ||
|
|
5401593279 | ||
|
|
0710e05479 | ||
|
|
1707d011a2 | ||
|
|
5e8d7944bd | ||
|
|
2d2727f7e8 | ||
|
|
c72282613d | ||
|
|
4ac62a107c | ||
|
|
a102199d5a | ||
|
|
3c799b8783 | ||
|
|
3fbbc7f620 | ||
|
|
461efa7f60 | ||
|
|
1321f8df50 | ||
|
|
a081f3a799 | ||
|
|
e532000ad0 | ||
|
|
8d0dc232d7 | ||
|
|
f5602f1e96 | ||
|
|
d9e1e2f58b | ||
|
|
5d56ed5378 |
152
.drone.yml
@@ -9,19 +9,69 @@ steps:
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- name: release
|
||||
image: golang:latest
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
JFA_GO_BUILT_BY:
|
||||
from_secret: BUILT_BY
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- curl -sL https://git.io/goreleaser | bash
|
||||
when:
|
||||
event: tag
|
||||
- curl -sL https://git.io/goreleaser > ../goreleaser
|
||||
- chmod +x ../goreleaser
|
||||
- ./scripts/version.sh ../goreleaser
|
||||
- wget https://builds.hrfee.pw/upload.py -P ../
|
||||
- pip3 install requests
|
||||
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||
bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
|
||||
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
---
|
||||
name: docker-buildx
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build-deploy
|
||||
image: appleboy/drone-ssh
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /root/drone_rsa
|
||||
settings:
|
||||
host:
|
||||
from_secret: ssh2_host
|
||||
username:
|
||||
from_secret: ssh2_username
|
||||
port:
|
||||
from_secret: ssh2_port
|
||||
volumes:
|
||||
- /root/.ssh/docker-build:/root/drone_rsa
|
||||
key_path: /root/drone_rsa
|
||||
command_timeout: 50m
|
||||
script:
|
||||
- /mnt/buildx/jfa-go/build.sh stable
|
||||
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && BUILDRONE_KEY=$(cat /mnt/buildx/jfa-go/key) python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true'
|
||||
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
trigger:
|
||||
event:
|
||||
- tag
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/docker-build
|
||||
---
|
||||
name: jfa-go-git
|
||||
kind: pipeline
|
||||
@@ -29,22 +79,74 @@ type: docker
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /id_rsa
|
||||
- name: ssh_key2
|
||||
path: /id_rsa2
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser.sh
|
||||
- chmod +x goreleaser.sh
|
||||
- ./goreleaser.sh --snapshot --skip-publish --rm-dist
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --clean
|
||||
- wget https://builds.hrfee.pw/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go ./dist/*.tar.gz'
|
||||
- bash -c 'sftp -i /id_rsa2 -o StrictHostKeyChecking=no root@161.97.102.153:/mnt/redoc <<< $"put docs/swagger.json jfa-go.json"'
|
||||
- bash -c 'sftp -P 2022 -i /id_rsa -o StrictHostKeyChecking=no root@161.97.102.153:/repo/incoming <<< $"put dist/*.deb"'
|
||||
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go"'
|
||||
# - bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "reprepro -Vb /repo remove trusty-unstable jfa-go-tray"'
|
||||
- bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "repo-process-deb trusty"'
|
||||
bash -c 'ssh -i /id_rsa root@161.97.102.153 -p 2022 "rm /repo/incoming/*.deb"'
|
||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.zip ./dist/*.rpm ./dist/*.apk --tag internal-git=true'
|
||||
environment:
|
||||
BUILDRONE_KEY:
|
||||
from_secret: BUILDRONE_KEY
|
||||
JFA_GO_BUILT_BY:
|
||||
from_secret: BUILT_BY
|
||||
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/id_rsa_packaging
|
||||
- name: ssh_key2
|
||||
host:
|
||||
path: /root/.ssh/docker-build
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
- go1.16
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
name: docker-buildx-unstable
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: build-deploy
|
||||
image: appleboy/drone-ssh
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
path: /root/drone_rsa
|
||||
settings:
|
||||
host:
|
||||
from_secret: ssh2_host
|
||||
username:
|
||||
from_secret: ssh2_username
|
||||
port:
|
||||
from_secret: ssh2_port
|
||||
volumes:
|
||||
- /root/.ssh/docker-build:/root/drone_rsa
|
||||
key_path: /root/drone_rsa
|
||||
command_timeout: 50m
|
||||
script:
|
||||
- /mnt/buildx/jfa-go/build.sh
|
||||
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
- pip3 install requests
|
||||
- bash -c 'cd /mnt/buildx/jfa-go/jfa-go && BUILDRONE_KEY=$(cat /mnt/buildx/jfa-go/key) python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true'
|
||||
- rm -f /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
@@ -52,6 +154,10 @@ trigger:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
||||
volumes:
|
||||
- name: ssh_key
|
||||
host:
|
||||
path: /root/.ssh/docker-build
|
||||
---
|
||||
name: jfa-go-pr
|
||||
kind: pipeline
|
||||
@@ -59,15 +165,11 @@ type: docker
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:latest
|
||||
image: hrfee/jfa-go-build-docker:latest
|
||||
commands:
|
||||
- apt update -y
|
||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||
- apt install nodejs
|
||||
- curl -sL https://git.io/goreleaser > goreleaser.sh
|
||||
- chmod +x goreleaser.sh
|
||||
- ./goreleaser.sh --snapshot --skip-publish --rm-dist
|
||||
- curl -sL https://git.io/goreleaser > goreleaser
|
||||
- chmod +x goreleaser
|
||||
- ./scripts/version.sh ./goreleaser --snapshot --skip-publish --clean
|
||||
|
||||
trigger:
|
||||
event:
|
||||
|
||||
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: hrfee
|
||||
ko_fi: hrfee
|
||||
custom: https://www.buymeacoffee.com/hrfee
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,7 +7,7 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
#### Read the [FAQ](https://github.com/hrfee/jfa-go/wiki/FAQ) first!
|
||||
#### Read the [FAQ](https://wiki.jfa-go.com/docs/faq/) first!
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
@@ -19,7 +19,10 @@ What to do to reproduce the problem.
|
||||
|
||||
**Logs**
|
||||
|
||||
When you notice the problem, check the output of `jfa-go`. If the problem is not obvious (e.g a panic (red text) or 'ERROR' log), re-run jfa-go with the `-debug` argument and reproduce the problem. You should then take a screenshot of the output, or paste it here, preferably between \`\`\` tags (e.g \`\`\``Log here`\`\`\`). Remember to censor any personal information.
|
||||
**If you're using a build with a tray icon, right-click on it and press "Open logs" to access your logs.**
|
||||
|
||||
When you notice the problem, check the output of `jfa-go` or get the logs by pressing the "Logs" button in the Settings tab. If the problem is not obvious (e.g a panic (red text) or 'ERROR' log), re-run jfa-go with the `-debug` argument and reproduce the problem. You should then take a screenshot of the output, or paste it here, preferably between \`\`\` tags (e.g \`\`\``Log here`\`\`\`). Remember to censor any personal information.
|
||||
|
||||
|
||||
If nothing catches your eye in the log, access the admin page via your browser, go into the console (Right click > Inspect Element > Console), refresh, reproduce the problem then paste the output here in the same way as above.
|
||||
|
||||
|
||||
16
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
node_modules/
|
||||
site/node_modules/
|
||||
site/out/
|
||||
site/tempts/
|
||||
mail/*.html
|
||||
dist/
|
||||
build/
|
||||
@@ -6,5 +9,18 @@ data/
|
||||
version.go
|
||||
notes
|
||||
docs/*
|
||||
lang/langtostruct.py
|
||||
config-payload.json
|
||||
!docs/go.mod
|
||||
server.key
|
||||
server.pem
|
||||
server.crt
|
||||
instructions-debian.txt
|
||||
cl.md
|
||||
./telegram/
|
||||
mautrix/
|
||||
tempts/
|
||||
matacc.txt
|
||||
scripts/langmover/lang
|
||||
scripts/langmover/lang2
|
||||
scripts/langmover/out
|
||||
|
||||
166
.goreleaser.yml
@@ -8,50 +8,170 @@ before:
|
||||
hooks:
|
||||
- go mod download
|
||||
- rm -rf data/web
|
||||
- mkdir -p data
|
||||
- cp -r static data/web
|
||||
- cp -r css data/web/
|
||||
- mkdir -p data/web/css
|
||||
- bash -c 'cp -r static/* data/web/'
|
||||
- npm install
|
||||
- cp node_modules/a17t/dist/a17t.css data/web/css/
|
||||
- npm install esbuild
|
||||
- cp node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 data/web/css/
|
||||
- cp -r html data/
|
||||
- node scripts/missing-colors.js html data/html
|
||||
- cp -r lang data/
|
||||
- python3 config/fixconfig.py -i config/config-base.json -o data/config-base.json
|
||||
- python3 config/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
- python3 mail/generate.py -o data/
|
||||
- python3 version.py {{.Version}} version.go
|
||||
- bash -c 'npx esbuild ts/*.ts ts/modules/*.ts --outdir=./data/web/js/ --minify'
|
||||
- go get -u github.com/swaggo/swag/cmd/swag
|
||||
- cp LICENSE data/
|
||||
- cp jfa-go.service data/
|
||||
- python3 scripts/enumerate_config.py -i config/config-base.json -o data/config-base.json
|
||||
- python3 scripts/generate_ini.py -i config/config-base.json -o data/config-default.ini
|
||||
- python3 scripts/compile_mjml.py -o data/
|
||||
- rm -rf tempts
|
||||
- cp -r ts tempts
|
||||
- scripts/dark-variant.sh tempts
|
||||
- scripts/dark-variant.sh tempts/modules
|
||||
- mkdir -p data/web/js
|
||||
- npx esbuild --target=es6 --bundle tempts/admin.ts --outfile=./data/web/js/admin.js --minify
|
||||
- npx esbuild --target=es6 --bundle tempts/user.ts --outfile=./data/web/js/user.js --minify
|
||||
- npx esbuild --target=es6 --bundle tempts/pwr.ts --outfile=./data/web/js/pwr.js --minify
|
||||
- npx esbuild --target=es6 --bundle tempts/form.ts --outfile=./data/web/js/form.js --minify
|
||||
- npx esbuild --target=es6 --bundle tempts/setup.ts --outfile=./data/web/js/setup.js --minify
|
||||
- npx esbuild --target=es6 --bundle tempts/crash.ts --outfile=./data/crash.js --minify
|
||||
- rm -r tempts
|
||||
- npx esbuild --bundle css/base.css --outfile=./data/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||
- cp html/crash.html data/
|
||||
- npx tailwindcss -i data/web/css/bundle.css -o data/bundle.css --content "html/crash.html"
|
||||
- node scripts/inline.js root data data/crash.html data/crash.html
|
||||
- rm data/bundle.css
|
||||
- npx tailwindcss -i data/web/css/bundle.css -o data/web/css/bundle.css
|
||||
- mv data/crash.html data/html/
|
||||
- go install github.com/swaggo/swag/cmd/swag@latest
|
||||
- swag init -g main.go
|
||||
- mv data/web/css/bundle.css data/web/css/{{.Env.JFA_GO_CSS_VERSION}}bundle.css
|
||||
builds:
|
||||
- dir: ./
|
||||
- id: notray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- amd64
|
||||
- id: windows-tray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
- CXX=x86_64-w64-mingw32-g++
|
||||
flags:
|
||||
- -tags=tray
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}" -H=windowsgui
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- id: linux-tray
|
||||
dir: ./
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
flags:
|
||||
- -tags=tray
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}} -X main.buildTimeUnix={{.Env.JFA_GO_BUILD_TIME}} -X main.builtBy="{{.Env.JFA_GO_BUILT_BY}}"
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
amd64: x86_64
|
||||
files:
|
||||
- data/*
|
||||
- data/**/*
|
||||
- data/**/**/*
|
||||
- id: windows-tray
|
||||
builds:
|
||||
- windows-tray
|
||||
format: zip
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_TrayIcon_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
{{- else }}{{- title .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
- id: linux-tray
|
||||
builds:
|
||||
- linux-tray
|
||||
format: zip
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_TrayIcon_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
{{- else }}{{- title .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
- id: notray
|
||||
builds:
|
||||
- notray
|
||||
format: zip
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_{{ .Version }}_
|
||||
{{- if eq .Os "darwin" }}macOS
|
||||
{{- else }}{{- title .Os }}{{ end }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "git-{{.ShortCommit}}"
|
||||
name_template: "0.0.0-{{ .Env.JFA_GO_NFPM_EPOCH }}"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
nfpms:
|
||||
- id: notray
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go
|
||||
homepage: https://github.com/hrfee/jfa-go
|
||||
description: A web app for managing users on Jellyfin
|
||||
maintainer: Harvey Tindall <hrfee@hrfee.dev>
|
||||
license: MIT
|
||||
vendor: hrfee.dev
|
||||
version_metadata: git
|
||||
builds:
|
||||
- notray
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/licenses/jfa-go
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
- id: tray
|
||||
file_name_template: '{{ .ProjectName }}{{ if .IsSnapshot }}-git{{ end }}_TrayIcon_{{ .Arch }}_{{ if .IsSnapshot }}{{ .ShortCommit }}{{ else }}v{{ .Version }}{{ end }}'
|
||||
package_name: jfa-go-tray
|
||||
homepage: https://github.com/hrfee/jfa-go
|
||||
description: A web app for managing users on Jellyfin
|
||||
maintainer: Harvey Tindall <hrfee@hrfee.dev>
|
||||
license: MIT
|
||||
vendor: hrfee.dev
|
||||
version_metadata: git
|
||||
builds:
|
||||
- linux-tray
|
||||
contents:
|
||||
- src: ./LICENSE
|
||||
dst: /usr/share/licenses/jfa-go
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
overrides:
|
||||
deb:
|
||||
conflicts:
|
||||
- jfa-go
|
||||
replaces:
|
||||
- jfa-go
|
||||
dependencies:
|
||||
- libayatana-appindicator
|
||||
rpm:
|
||||
dependencies:
|
||||
- libappindicator-gtk3
|
||||
apk:
|
||||
dependencies:
|
||||
- libayatana-appindicator
|
||||
|
||||
44
CONTRIBUTING.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
title: "Building/Contributing for developers"
|
||||
date: 2021-07-25T00:33:36+01:00
|
||||
draft: false
|
||||
---
|
||||
# Code
|
||||
I use 4 spaces for indentation. Go should ideally be formatted with `goimports` and/or `gofmt`. I don't use a formatter on typescript, so don't worry about that.
|
||||
|
||||
Code in Go should ideally use `PascalCase` for exported values, and `camelCase` for non-exported, JSON for transferring data should use `snake_case`, and Typescript should use `camelCase`. Forgive me for my many inconsistencies in this, and feel free to fix them if you want.
|
||||
|
||||
Functions in Go that need to access `*appContext` should be generally be receivers, except when the behaviour could be seen as somewhat independent from it (`email.go` is the best example, its behaviour is broadly independent from the main app except from a couple config values).
|
||||
|
||||
|
||||
# Compiling
|
||||
|
||||
The Makefile is more suited towards development than other build methods, and provides separate build stages to speed up compilation when only making changes to specific aspects of the project.
|
||||
|
||||
Prefix each of these with `make DEBUG=on `:
|
||||
* `all` will download deps and build everything. The executable and data will be placed in `build`. This is only necessary the first time.
|
||||
* `npm` will download all node.js build-time dependencies.
|
||||
* `compile` will only compile go code into the `build/jfa-go` executable.
|
||||
* `typescript` will compile typescript w/ sourcemaps into `build/data/web/js`.
|
||||
* `bundle-css` will bundle CSS and place it in `build/data/web/css`.
|
||||
* `inline` will inline the css and javascript used in the single-file crash report webpage.
|
||||
* `configuration` will generate the `config-base.json` (used to render settings in the web ui) and `config-default.ini` and put them in `build/data`.
|
||||
* `email` will compile email mjml, and copy the text versions in to `build/data`.
|
||||
* `swagger`: generates swagger documentation for the API.
|
||||
* `copy` will copy iconography, html, language files and static data into `build/data`.
|
||||
|
||||
## Environment variables
|
||||
|
||||
* `DEBUG=on/off`: If on, compiles with type-checking for typescript, sourcemaps, non-minified css and no symbol stripping.
|
||||
* `INTERNAL=on/off`: Whether or not to embed file assets into the binary itself, or store them separately beside the binary.
|
||||
* `UPDATER=on/off/docker`: Enable/Disable the updater, or set a special update type (currently only docker, which disables self-updating the binary).
|
||||
* `TRAY=on/off`: Enable/disable the tray icon, which lets you start/stop/autostart on login. For linux, requires `libappindicator3-dev` for debian or the equivalent on other distributions.
|
||||
* `GOESBUILD=on`: Use a locally installed `esbuild` binary. NPM doesn't provide builds for all os/architectures, so `npx esbuild` might not work for you, so the binary is compiled/installed with `go get`.
|
||||
* `GOBINARY=<path to go>`: Alternative path to go executable. Useful for testing with unstable go releases.
|
||||
* `VERSION=v<semver>`: Alternative verision number, useful to test update functionality.
|
||||
* `COMMIT=<short commit>`: Self explanatory.
|
||||
* `LDFLAGS=<ldflags>`: Passed to `go build -ldflags`.
|
||||
* `E2EE=on/off`: Enable/disable end-to-end encryption support for Matrix, which is currently very broken. Must subsequently be enabled (with Advanced settings enabled) in Settings > Matrix.
|
||||
* `TAGS=<tags>`: Passed to `go build -tags`.
|
||||
* `OS=<os>`: Unrelated to GOOS, if set to `windows`, `-H=windowsgui` is passed to ldflags, which stops a windows terminal popping up when run.
|
||||
* `RACE=on/off`: If on, compiles with the go race detector included.
|
||||
24
Dockerfile
@@ -1,19 +1,29 @@
|
||||
FROM golang:latest AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:latest AS support
|
||||
|
||||
COPY . /opt/build
|
||||
|
||||
RUN apt update -y \
|
||||
&& apt install build-essential python3-pip curl software-properties-common sed upx -y \
|
||||
&& (curl -sL https://deb.nodesource.com/setup_14.x | bash -) \
|
||||
&& apt install nodejs \
|
||||
&& (cd /opt/build; make all; make compress) \
|
||||
&& sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install build-essential python3-pip curl software-properties-common sed -y \
|
||||
&& (curl -sL https://deb.nodesource.com/setup_current.x | bash -) \
|
||||
&& apt-get install nodejs \
|
||||
&& (cd /opt/build; make configuration npm email typescript variants-html bundle-css inline-css swagger copy INTERNAL=off GOESBUILD=on) \
|
||||
&& sed -i 's#id="password_resets-watch_directory" placeholder="/config/jellyfin"#id="password_resets-watch_directory" value="/jf" disabled#g' /opt/build/build/data/html/setup.html
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:latest AS build
|
||||
ARG TARGETARCH
|
||||
ENV GOARCH=$TARGETARCH
|
||||
|
||||
COPY --from=support /opt/build /opt/build
|
||||
|
||||
RUN (cd /opt/build; make compile INTERNAL=off UPDATER=docker)
|
||||
|
||||
FROM golang:latest
|
||||
|
||||
COPY --from=build /opt/build/build /opt/jfa-go
|
||||
|
||||
EXPOSE 8056
|
||||
EXPOSE 8057
|
||||
|
||||
CMD [ "/opt/jfa-go/jfa-go", "-data", "/data" ]
|
||||
|
||||
|
||||
5
LICENSE
@@ -1,6 +1,8 @@
|
||||
---jfa-go---
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Harvey Tindall
|
||||
Copyright (c) 2023 Harvey Tindall
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -19,3 +21,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
200
Makefile
@@ -1,65 +1,193 @@
|
||||
GOESBUILD ?= off
|
||||
ifeq ($(GOESBUILD), on)
|
||||
ESBUILD := esbuild
|
||||
else
|
||||
ESBUILD := npx esbuild
|
||||
endif
|
||||
GOBINARY ?= go
|
||||
|
||||
CSSVERSION ?= v3
|
||||
|
||||
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
|
||||
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown)
|
||||
BUILDTIME ?= $(shell date +%s)
|
||||
|
||||
UPDATER ?= off
|
||||
LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.cssVersion=$(CSSVERSION) -X main.buildTimeUnix=$(BUILDTIME) $(if $(BUILTBY),-X 'main.builtBy=$(BUILTBY)',)
|
||||
ifeq ($(UPDATER), on)
|
||||
LDFLAGS := $(LDFLAGS) -X main.updater=binary
|
||||
else ifneq ($(UPDATER), off)
|
||||
LDFLAGS := $(LDFLAGS) -X main.updater=$(UPDATER)
|
||||
endif
|
||||
|
||||
|
||||
|
||||
INTERNAL ?= on
|
||||
TRAY ?= off
|
||||
E2EE ?= off
|
||||
TAGS := -tags "
|
||||
|
||||
ifeq ($(INTERNAL), on)
|
||||
DATA := data
|
||||
else
|
||||
DATA := build/data
|
||||
TAGS := $(TAGS) external
|
||||
endif
|
||||
|
||||
ifeq ($(TRAY), on)
|
||||
TAGS := $(TAGS) tray
|
||||
endif
|
||||
|
||||
ifeq ($(E2EE), on)
|
||||
TAGS := $(TAGS) e2ee
|
||||
endif
|
||||
|
||||
TAGS := $(TAGS)"
|
||||
|
||||
OS := $(shell go env GOOS)
|
||||
ifeq ($(TRAY)$(OS), onwindows)
|
||||
LDFLAGS := $(LDFLAGS) -H=windowsgui
|
||||
endif
|
||||
|
||||
DEBUG ?= off
|
||||
ifeq ($(DEBUG), on)
|
||||
SOURCEMAP := --sourcemap
|
||||
TYPECHECK := npx tsc -noEmit --project ts/tsconfig.json
|
||||
# jank
|
||||
COPYTS := rm -r $(DATA)/web/js/ts; cp -r tempts $(DATA)/web/js/ts
|
||||
UNCSS := cp $(DATA)/web/css/bundle.css $(DATA)/bundle.css
|
||||
# TAILWIND := --content ""
|
||||
else
|
||||
LDFLAGS := -s -w $(LDFLAGS)
|
||||
SOURCEMAP :=
|
||||
COPYTS :=
|
||||
TYPECHECK :=
|
||||
UNCSS := npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/bundle.css --content "html/crash.html"
|
||||
# UNCSS := npx uncss $(DATA)/crash.html --csspath web/css --output $(DATA)/bundle.css
|
||||
TAILWIND :=
|
||||
endif
|
||||
|
||||
RACE ?= off
|
||||
ifeq ($(RACE), on)
|
||||
RACEDETECTOR := -race
|
||||
else
|
||||
RACEDETECTOR :=
|
||||
endif
|
||||
|
||||
ifeq (, $(shell which esbuild))
|
||||
ESBUILDINSTALL := go install github.com/evanw/esbuild/cmd/esbuild@latest
|
||||
else
|
||||
ESBUILDINSTALL :=
|
||||
endif
|
||||
|
||||
ifeq ($(GOESBUILD), on)
|
||||
NPMIGNOREOPTIONAL := --no-optional
|
||||
NPMOPTS := $(NPMIGNOREOPTIONAL); $(ESBUILDINSTALL)
|
||||
else
|
||||
NPMOPTS :=
|
||||
endif
|
||||
|
||||
|
||||
npm:
|
||||
$(info installing npm dependencies)
|
||||
npm install
|
||||
npm install $(NPMOPTS)
|
||||
|
||||
configuration:
|
||||
$(info Fixing config-base)
|
||||
-mkdir -p build/data
|
||||
python3 config/fixconfig.py -i config/config-base.json -o build/data/config-base.json
|
||||
-mkdir -p $(DATA)
|
||||
python3 scripts/enumerate_config.py -i config/config-base.json -o $(DATA)/config-base.json
|
||||
$(info Generating config-default.ini)
|
||||
python3 config/generate_ini.py -i config/config-base.json -o build/data/config-default.ini
|
||||
python3 scripts/generate_ini.py -i config/config-base.json -o $(DATA)/config-default.ini
|
||||
|
||||
email:
|
||||
$(info Generating email html)
|
||||
python3 mail/generate.py -o build/data/
|
||||
python3 scripts/compile_mjml.py -o $(DATA)/
|
||||
|
||||
typescript:
|
||||
$(TYPECHECK)
|
||||
$(adding dark variants to typescript)
|
||||
rm -rf tempts
|
||||
cp -r ts tempts
|
||||
scripts/dark-variant.sh tempts
|
||||
scripts/dark-variant.sh tempts/modules
|
||||
$(info compiling typescript)
|
||||
-mkdir -p build/data/web/js
|
||||
-npx esbuild ts/*.ts ts/modules/*.ts --outdir=./build/data/web/js/
|
||||
|
||||
ts-debug:
|
||||
$(info compiling typescript w/ sourcemaps)
|
||||
-mkdir -p build/data/web/js
|
||||
-npx esbuild ts/*.ts ts/modules/*.ts --sourcemap --outdir=./build/data/web/js/
|
||||
-rm -r build/data/web/js/ts
|
||||
$(info copying typescript)
|
||||
cp -r ts build/data/web/js
|
||||
mkdir -p $(DATA)/web/js
|
||||
$(ESBUILD) --target=es6 --bundle tempts/admin.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/admin.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/user.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/user.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/pwr.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/pwr.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/form.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/form.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/setup.ts $(SOURCEMAP) --outfile=./$(DATA)/web/js/setup.js --minify
|
||||
$(ESBUILD) --target=es6 --bundle tempts/crash.ts --outfile=./$(DATA)/crash.js --minify
|
||||
$(COPYTS)
|
||||
|
||||
swagger:
|
||||
go get github.com/swaggo/swag/cmd/swag
|
||||
$(GOBINARY) install github.com/swaggo/swag/cmd/swag@latest
|
||||
swag init -g main.go
|
||||
|
||||
version:
|
||||
python3 version.py auto version.go
|
||||
|
||||
compile:
|
||||
$(info Downloading deps)
|
||||
go mod download
|
||||
$(GOBINARY) mod download
|
||||
$(info Building)
|
||||
mkdir -p build
|
||||
CGO_ENABLED=0 go build -o build/jfa-go *.go
|
||||
$(GOBINARY) build $(RACEDETECTOR) -ldflags="$(LDFLAGS)" $(TAGS) -o build/jfa-go
|
||||
|
||||
compress:
|
||||
upx --lzma build/jfa-go
|
||||
|
||||
copy:
|
||||
$(info copying css)
|
||||
-mkdir -p build/data/web/css
|
||||
cp -r css build/data/web/
|
||||
cp node_modules/a17t/dist/a17t.css build/data/web/css/
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 build/data/web/css/
|
||||
$(info copying html)
|
||||
cp -r html build/data/
|
||||
$(info copying static data)
|
||||
-mkdir -p build/data/web
|
||||
cp -r static/* build/data/web/
|
||||
$(info copying language files)
|
||||
cp -r lang build/data/
|
||||
bundle-css:
|
||||
mkdir -p $(DATA)/web/css
|
||||
$(info copying fonts)
|
||||
cp -r node_modules/remixicon/fonts/remixicon.css node_modules/remixicon/fonts/remixicon.woff2 $(DATA)/web/css/
|
||||
$(info bundling css)
|
||||
$(ESBUILD) --bundle css/base.css --outfile=$(DATA)/web/css/bundle.css --external:remixicon.css --external:../fonts/hanken* --minify
|
||||
npx tailwindcss -i $(DATA)/web/css/bundle.css -o $(DATA)/web/css/bundle.css $(TAILWIND)
|
||||
# npx postcss -o $(DATA)/web/css/bundle.css $(DATA)/web/css/bundle.css
|
||||
|
||||
inline-css:
|
||||
cp html/crash.html $(DATA)/crash.html
|
||||
$(UNCSS)
|
||||
node scripts/inline.js root $(DATA) $(DATA)/crash.html $(DATA)/crash.html
|
||||
rm $(DATA)/bundle.css
|
||||
|
||||
variants-html:
|
||||
$(info copying html)
|
||||
cp -r html $(DATA)/
|
||||
$(info adding dark variants to html)
|
||||
node scripts/missing-colors.js html $(DATA)/html
|
||||
|
||||
copy:
|
||||
$(info copying crash page)
|
||||
mv $(DATA)/crash.html $(DATA)/html/
|
||||
$(info copying static data)
|
||||
mkdir -p $(DATA)/web
|
||||
cp -r static/* $(DATA)/web/
|
||||
$(info copying systemd service)
|
||||
cp jfa-go.service $(DATA)/
|
||||
$(info copying language files)
|
||||
cp -r lang $(DATA)/
|
||||
cp LICENSE $(DATA)/
|
||||
mv $(DATA)/web/css/bundle.css $(DATA)/web/css/$(CSSVERSION)bundle.css
|
||||
|
||||
# internal-files:
|
||||
# python3 scripts/embed.py internal
|
||||
#
|
||||
# external-files:
|
||||
# python3 scripts/embed.py external
|
||||
# -mkdir -p build
|
||||
# $(info copying internal data into build/)
|
||||
# cp -r data build/
|
||||
|
||||
install:
|
||||
cp -r build $(DESTDIR)/jfa-go
|
||||
|
||||
all: configuration npm email version typescript swagger compile copy
|
||||
debug: configuration npm email version ts-debug swagger compile copy
|
||||
clean:
|
||||
-rm -r $(DATA)
|
||||
-rm -r build
|
||||
-rm mail/*.html
|
||||
-rm docs/docs.go docs/swagger.json docs/swagger.yaml
|
||||
go clean
|
||||
|
||||
quick: configuration typescript variants-html bundle-css inline-css copy compile
|
||||
|
||||
all: configuration npm email typescript variants-html bundle-css inline-css swagger copy compile
|
||||
|
||||
149
README.md
@@ -1,84 +1,161 @@
|
||||
# 
|
||||

|
||||
[](https://drone.hrfee.dev/hrfee/jfa-go)
|
||||
[](https://hub.docker.com/r/hrfee/jfa-go)
|
||||
[](https://weblate.jfa-go.com/engage/jfa-go/)
|
||||
[](https://wiki.jfa-go.com)
|
||||
[](https://discord.com/invite/MrtvuQmyhP)
|
||||
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
##### Downloads:
|
||||
##### [docker](#docker) | [debian/ubuntu](#debian) | [arch (aur)](#aur) | [other platforms](#other-platforms)
|
||||
|
||||
I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) in Go mainly as a learning experience, but also to slightly improve speeds and efficiency.
|
||||
---
|
||||
## Project Status: Active-ish
|
||||
Studies mean I can't work on this project a lot outside of breaks, however I hope i'll be able to fit in general support and things like bug fixes into my time. New features and such will likely come in short bursts throughout the year (if they do at all).
|
||||
|
||||
#### Does/Will it still work?
|
||||
jfa-go currently works on Jellyfin 10.8.9, the latest version. I should be able to maintain compatability in the future, unless any big changes occur.
|
||||
|
||||
#### Alternatives
|
||||
If you want a bit more of a guarantee of support, I've seen these projects mentioned although haven't tried them myself.
|
||||
|
||||
* [Wizarr](https://github.com/Wizarrrr/wizarr) focuses on invites, and also includes some Discord & Ombi integration.
|
||||
* [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) is a fork of Overseerr, which can manage users and mainly acts as an Ombi alternative.
|
||||
* [Organizr](https://github.com/causefx/Organizr) doesn't focus on Jellyfin, but allows putting self-hosted services into "tabs" on a central page, and allows creating users, which lets one control who can access what.
|
||||
---
|
||||
jfa-go is a user management app for [Jellyfin](https://github.com/jellyfin/jellyfin) (and now [Emby](https://emby.media/)) that provides invite-based account creation as well as other features that make one's instance much easier to manage.
|
||||
|
||||
a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (original naming for both, ik
|
||||
😂).
|
||||
|
||||
#### Features
|
||||
* 🧑 Invite based account creation: Sends invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* 🧑 Invite based account creation: Send invites to your friends or family, and let them choose their own username and password without relying on you.
|
||||
* Send invites via a link and/or email
|
||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
||||
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
|
||||
* Password validation: Ensure users choose a strong password.
|
||||
* CAPTCHAs can be enabled to avoid bots
|
||||
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
|
||||
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
|
||||
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
|
||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||
* Email addresses can optionally be used instead of usernames
|
||||
* 🔑 Password resets: When user's forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email.
|
||||
* Notifications: Get notified when someone creates an account, or an invite expires.
|
||||
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to them via email/telegram.
|
||||
* Admin Notifications: Get notified when someone creates an account, or an invite expires.
|
||||
* 📣 Announcements: Bulk message your users with announcements about your server.
|
||||
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
||||
* Enables the usage of jfa-go by multiple people
|
||||
* 🌓 Customizable look
|
||||
* 🌓 Customizations
|
||||
* Customize emails with variables and markdown
|
||||
* Specify contact and help messages to appear in emails and pages
|
||||
* Light and dark themes available
|
||||
|
||||
## Interface
|
||||
#### Interface
|
||||
<p align="center">
|
||||
<img src="images/demo.gif" width="100%"></img>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
||||
<img src="images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
||||
<img src="images/invites.png" width="31%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
||||
<img src="images/accounts.png" width="31%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
||||
<img src="images/create.png" width="31%" style="margin-right: 1.5%;" alt="Accounts creation"></img>
|
||||
</p>
|
||||
|
||||
#### Install
|
||||
|
||||
Available on the AUR as [jfa-go](https://aur.archlinux.org/packages/jfa-go/) or [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/).
|
||||
**Note**: `TrayIcon` builds include a tray icon to start/stop/restart, and an option to automatically start when you log-in to your computer. For Linux users, these builds depend on the `libappindicator3-1`/`libappindicator-gtk3`/`libappindicator` package for Debian/Ubuntu, Fedora, and Alpine respectively.
|
||||
|
||||
For other platforms, grab an archive from the release section for your platform (or nightly builds [here](https://builds.hrfee.pw/view/hrfee/jfa-go)), and extract `jfa-go` and `data` to the same directory.
|
||||
* For linux users, you can place them inside `/opt/jfa-go` and then run
|
||||
`sudo ln -s /opt/jfa-go/jfa-go /usr/bin/jfa-go` to place it in your PATH.
|
||||
|
||||
Run the executable to start.
|
||||
|
||||
For [docker](https://hub.docker.com/repository/docker/hrfee/jfa-go), run:
|
||||
```
|
||||
##### [Docker](https://hub.docker.com/r/hrfee/jfa-go)
|
||||
```sh
|
||||
docker create \
|
||||
--name "jfa-go" \ # Whatever you want to name it
|
||||
-p 8056:8056 \
|
||||
# -p 8057:8057 if using tls
|
||||
-v /path/to/.config/jfa-go:/data \ # Path to wherever you want to store the config file and other data
|
||||
-v /path/to/jellyfin:/jf \ # Path to jellyfin config directory
|
||||
-v /path/to/jellyfin:/jf \ # Path to Jellyfin config directory, ignore if using Emby
|
||||
-v /etc/localtime:/etc/localtime:ro \ # Makes sure time is correct
|
||||
hrfee/jfa-go # hrfee/jfa-go:unstable for latest build from git
|
||||
```
|
||||
|
||||
##### [Debian/Ubuntu](https://apt.hrfee.dev)
|
||||
```sh
|
||||
sudo apt-get update && sudo apt-get install curl apt-transport-https gnupg
|
||||
curl https://apt.hrfee.dev/hrfee.pubkey.gpg | sudo apt-key add -
|
||||
|
||||
# For stable releases
|
||||
echo "deb https://apt.hrfee.dev trusty main" | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
# ------
|
||||
# For unstable releases
|
||||
echo "deb https://apt.hrfee.dev trusty-unstable main" | sudo tee /etc/apt/sources.list.d/hrfee.list
|
||||
# ------
|
||||
|
||||
sudo apt-get update
|
||||
|
||||
# For servers
|
||||
sudo apt-get install jfa-go
|
||||
# ------
|
||||
# For desktops/servers with GUI (has dependencies)
|
||||
sudo apt-get install jfa-go-tray
|
||||
# ------
|
||||
```
|
||||
|
||||
##### Arch
|
||||
Available on the AUR as:
|
||||
* [jfa-go](https://aur.archlinux.org/packages/jfa-go/) (stable)
|
||||
* [jfa-go-bin](https://aur.archlinux.org/packages/jfa-go) (pre-compiled, stable)
|
||||
* [jfa-go-git](https://aur.archlinux.org/packages/jfa-go-git/) (nightly)
|
||||
|
||||
##### Other platforms
|
||||
Download precompiled binaries from:
|
||||
* [The releases section](https://github.com/hrfee/jfa-go/releases) (stable)
|
||||
* [Buildrone](https://builds.hrfee.dev/view/hrfee/jfa-go) (nightly)
|
||||
|
||||
unzip the `jfa-go`/`jfa-go.exe` executable to somewhere useful.
|
||||
* For \*nix/macOS users, `chmod +x jfa-go` then place it somewhere in your PATH like `/usr/bin`.
|
||||
|
||||
Run the executable to start.
|
||||
|
||||
|
||||
#### Build from source
|
||||
If you're using docker, a Dockerfile is provided that builds from source.
|
||||
|
||||
Otherwise, full build instructions can be found [here](https://github.com/hrfee/jfa-go/wiki/Build).
|
||||
Otherwise, full build instructions can be found [here](https://wiki.jfa-go.com/docs/build/).
|
||||
|
||||
#### Usage
|
||||
Simply run `jfa-go` to start the application. A setup wizard will start on `localhost:8056` (or your own specified address). Upon completion, refresh the page.
|
||||
|
||||
Note: jfa-go does not run as a daemon by default. You'll need to figure this out yourself.
|
||||
|
||||
```
|
||||
Usage of ./jfa-go:
|
||||
-config string
|
||||
alternate path to config file. (default "~/.config/jfa-go/config.ini")
|
||||
-data string
|
||||
alternate path to data directory. (default "~/.config/jfa-go")
|
||||
Usage of jfa-go:
|
||||
start
|
||||
start jfa-go as a daemon and run in the background.
|
||||
stop
|
||||
stop a daemonized instance of jfa-go.
|
||||
systemd
|
||||
generate a systemd .service file.
|
||||
|
||||
-config, -c string
|
||||
alternate path to config file. (default "/home/hrfee/.config/jfa-go/config.ini")
|
||||
-data, -d string
|
||||
alternate path to data directory. (default "/home/hrfee/.config/jfa-go")
|
||||
-debug
|
||||
Enables debug logging and exposes pprof.
|
||||
Enables debug logging.
|
||||
-help, -h
|
||||
prints this message.
|
||||
-host string
|
||||
alternate address to host web ui on.
|
||||
-port int
|
||||
-port, -p int
|
||||
alternate port to host web ui on.
|
||||
-pprof
|
||||
Exposes pprof profiler on /debug/pprof.
|
||||
-swagger
|
||||
Enable swagger at /swagger/index.html
|
||||
```
|
||||
|
||||
#### Systemd
|
||||
jfa-go does not run as a daemon by default. Run `jfa-go systemd` to create a systemd `.service` file in your current directory, which you can copy into `~/.config/systemd/user` or somewhere else.
|
||||
|
||||
---
|
||||
|
||||
If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts` to:
|
||||
|
||||
* `XDG_CONFIG_DIR/jfa-go` (usually ~/.config/jfa-go) on \*nix systems,
|
||||
@@ -86,3 +163,15 @@ If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts`
|
||||
* `~/Library/Application Support/jfa-go` on macOS.
|
||||
|
||||
(or specify config/data path with `-config/-data` respectively.)
|
||||
|
||||
#### Contributing
|
||||
See [the wiki page](https://wiki.jfa-go.com/docs/dev/) or [CONTRIBUTING.md](https://github.com/hrfee/jfa-go/blob/main/CONTRIBUTING.md).
|
||||
##### Translation
|
||||
[](https://weblate.jfa-go.com/engage/jfa-go/)
|
||||
|
||||
For translations, use the weblate instance [here](https://weblate.jfa-go.com/engage/jfa-go/). You can login with github.
|
||||
|
||||
#### Sponsors
|
||||
Big thanks to those who sponsor me. You can see them below:
|
||||
|
||||
[<img src="https://sponsors-endpoint.hrfee.pw/sponsor/avatar/0" width="35">](https://sponsors-endpoint.hrfee.pw/sponsor/profile/0)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
This branch is for experimenting with [a17t](https://a17t.miles.land/) to replace bootstrap. Page structure is pretty much done (except setup.html), so i'm currently integrating this with the main app and existing web code.
|
||||
|
||||
#### todo
|
||||
**general**
|
||||
* [x] modal implementation
|
||||
* [x] animations
|
||||
* [x] utilities
|
||||
* [x] CSS for light & dark
|
||||
|
||||
**admin**
|
||||
* [x] invites tab
|
||||
* [x] accounts tab
|
||||
* [x] settings tab
|
||||
* [x] modals
|
||||
* [ ] integration with existing code
|
||||
|
||||
**invites**
|
||||
* [x] page design
|
||||
* [ ] integration with existing code
|
||||
|
||||
#### screenshots
|
||||
##### dark
|
||||
<p>
|
||||
<img src="images/dark/invites.png" alt="invites" style="width: 32%; height: auto;">
|
||||
<img src="images/dark/accounts.png" alt="accounts" style="width: 32%; height: auto;">
|
||||
<img src="images/dark/settings.png" alt="settings" style="width: 32%; height: auto;">
|
||||
<img src="images/dark/login-modal.png" alt="login modal" style="width: 32%; height: auto;">
|
||||
<img src="images/dark/modify-settings.png" alt="modify user settings modal" style="width: 32%; height: auto;">
|
||||
</p>
|
||||
|
||||
##### light
|
||||
<p>
|
||||
<img src="images/light/invites.png" alt="invites" style="width: 32%; height: auto;">
|
||||
<img src="images/light/accounts.png" alt="accounts" style="width: 32%; height: auto;">
|
||||
<img src="images/light/settings.png" alt="settings" style="width: 32%; height: auto;">
|
||||
<img src="images/light/login-modal.png" alt="login modal" style="width: 32%; height: auto;">
|
||||
<img src="images/light/modify-settings.png" alt="modify user settings modal" style="width: 32%; height: auto;">
|
||||
</p>
|
||||
409
api-invites.go
Normal file
@@ -0,0 +1,409 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/itchyny/timefmt-go"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
func (app *appContext) checkInvites() {
|
||||
currentTime := time.Now()
|
||||
for _, data := range app.storage.GetInvites() {
|
||||
expiry := data.ValidTill
|
||||
if !currentTime.After(expiry) {
|
||||
continue
|
||||
}
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", data.Code)
|
||||
notify := data.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", data.Code)
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(data.Code, data, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", data.Code, err)
|
||||
} else {
|
||||
// Check whether notify "address" is an email address of Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", data.Code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", addr)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
app.storage.DeleteInvitesKey(data.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) checkInvite(code string, used bool, username string) bool {
|
||||
currentTime := time.Now()
|
||||
inv, match := app.storage.GetInvitesKey(code)
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
expiry := inv.ValidTill
|
||||
if currentTime.After(expiry) {
|
||||
app.debug.Printf("Housekeeping: Deleting old invite %s", code)
|
||||
notify := inv.Notify
|
||||
if emailEnabled && app.config.Section("notifications").Key("enabled").MustBool(false) && len(notify) != 0 {
|
||||
app.debug.Printf("%s: Expiry notification", code)
|
||||
var wait sync.WaitGroup
|
||||
for address, settings := range notify {
|
||||
if !settings["notify-expiry"] {
|
||||
continue
|
||||
}
|
||||
wait.Add(1)
|
||||
go func(addr string) {
|
||||
defer wait.Done()
|
||||
msg, err := app.email.constructExpiry(code, inv, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct expiry notification: %v", code, err)
|
||||
} else {
|
||||
// Check whether notify "address" is an email address of Jellyfin ID
|
||||
if strings.Contains(addr, "@") {
|
||||
err = app.email.send(msg, addr)
|
||||
} else {
|
||||
err = app.sendByID(msg, addr)
|
||||
}
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to send expiry notification: %v", code, err)
|
||||
} else {
|
||||
app.info.Printf("Sent expiry notification to %s", addr)
|
||||
}
|
||||
}
|
||||
}(address)
|
||||
}
|
||||
wait.Wait()
|
||||
}
|
||||
match = false
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
} else if used {
|
||||
del := false
|
||||
newInv := inv
|
||||
if newInv.RemainingUses == 1 {
|
||||
del = true
|
||||
app.storage.DeleteInvitesKey(code)
|
||||
} else if newInv.RemainingUses != 0 {
|
||||
// 0 means infinite i guess?
|
||||
newInv.RemainingUses--
|
||||
}
|
||||
newInv.UsedBy = append(newInv.UsedBy, []string{username, strconv.FormatInt(currentTime.Unix(), 10)})
|
||||
if !del {
|
||||
app.storage.SetInvitesKey(code, newInv)
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// @Summary Create a new invite.
|
||||
// @Produce json
|
||||
// @Param generateInviteDTO body generateInviteDTO true "New invite request object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Router /invites [post]
|
||||
// @Security Bearer
|
||||
// @tags Invites
|
||||
func (app *appContext) GenerateInvite(gc *gin.Context) {
|
||||
var req generateInviteDTO
|
||||
app.debug.Println("Generating new invite")
|
||||
gc.BindJSON(&req)
|
||||
currentTime := time.Now()
|
||||
validTill := currentTime.AddDate(0, req.Months, req.Days)
|
||||
validTill = validTill.Add(time.Hour*time.Duration(req.Hours) + time.Minute*time.Duration(req.Minutes))
|
||||
// make sure code doesn't begin with number
|
||||
inviteCode := shortuuid.New()
|
||||
_, err := strconv.Atoi(string(inviteCode[0]))
|
||||
for err == nil {
|
||||
inviteCode = shortuuid.New()
|
||||
_, err = strconv.Atoi(string(inviteCode[0]))
|
||||
}
|
||||
var invite Invite
|
||||
if req.Label != "" {
|
||||
invite.Label = req.Label
|
||||
}
|
||||
invite.Created = currentTime
|
||||
if req.MultipleUses {
|
||||
if req.NoLimit {
|
||||
invite.NoLimit = true
|
||||
} else {
|
||||
invite.RemainingUses = req.RemainingUses
|
||||
}
|
||||
} else {
|
||||
invite.RemainingUses = 1
|
||||
}
|
||||
invite.UserExpiry = req.UserExpiry
|
||||
if invite.UserExpiry {
|
||||
invite.UserMonths = req.UserMonths
|
||||
invite.UserDays = req.UserDays
|
||||
invite.UserHours = req.UserHours
|
||||
invite.UserMinutes = req.UserMinutes
|
||||
}
|
||||
invite.ValidTill = validTill
|
||||
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||
addressValid := false
|
||||
discord := ""
|
||||
app.debug.Printf("%s: Sending invite message", inviteCode)
|
||||
if discordEnabled && !strings.Contains(req.SendTo, "@") {
|
||||
users := app.discord.GetUsers(req.SendTo)
|
||||
if len(users) == 0 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||
} else if len(users) > 1 {
|
||||
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||
} else {
|
||||
invite.SendTo = req.SendTo
|
||||
addressValid = true
|
||||
discord = users[0].User.ID
|
||||
}
|
||||
} else if emailEnabled {
|
||||
addressValid = true
|
||||
invite.SendTo = req.SendTo
|
||||
}
|
||||
if addressValid {
|
||||
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
|
||||
} else {
|
||||
var err error
|
||||
if discord != "" {
|
||||
err = app.discord.SendDM(msg, discord)
|
||||
} else {
|
||||
err = app.email.send(msg, req.SendTo)
|
||||
}
|
||||
if err != nil {
|
||||
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Profile != "" {
|
||||
if _, ok := app.storage.GetProfileKey(req.Profile); ok {
|
||||
invite.Profile = req.Profile
|
||||
} else {
|
||||
invite.Profile = "Default"
|
||||
}
|
||||
}
|
||||
app.storage.SetInvitesKey(inviteCode, invite)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Get invites.
|
||||
// @Produce json
|
||||
// @Success 200 {object} getInvitesDTO
|
||||
// @Router /invites [get]
|
||||
// @Security Bearer
|
||||
// @tags Invites
|
||||
func (app *appContext) GetInvites(gc *gin.Context) {
|
||||
app.debug.Println("Invites requested")
|
||||
currentTime := time.Now()
|
||||
app.checkInvites()
|
||||
var invites []inviteDTO
|
||||
for _, inv := range app.storage.GetInvites() {
|
||||
_, months, days, hours, minutes, _ := timeDiff(inv.ValidTill, currentTime)
|
||||
invite := inviteDTO{
|
||||
Code: inv.Code,
|
||||
Months: months,
|
||||
Days: days,
|
||||
Hours: hours,
|
||||
Minutes: minutes,
|
||||
UserExpiry: inv.UserExpiry,
|
||||
UserMonths: inv.UserMonths,
|
||||
UserDays: inv.UserDays,
|
||||
UserHours: inv.UserHours,
|
||||
UserMinutes: inv.UserMinutes,
|
||||
Created: inv.Created.Unix(),
|
||||
Profile: inv.Profile,
|
||||
NoLimit: inv.NoLimit,
|
||||
Label: inv.Label,
|
||||
}
|
||||
if len(inv.UsedBy) != 0 {
|
||||
invite.UsedBy = map[string]int64{}
|
||||
for _, pair := range inv.UsedBy {
|
||||
// These used to be stored formatted instead of as a unix timestamp.
|
||||
unix, err := strconv.ParseInt(pair[1], 10, 64)
|
||||
if err != nil {
|
||||
date, err := timefmt.Parse(pair[1], app.datePattern+" "+app.timePattern)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to parse usedBy time: %v", err)
|
||||
}
|
||||
unix = date.Unix()
|
||||
}
|
||||
invite.UsedBy[pair[0]] = unix
|
||||
}
|
||||
}
|
||||
invite.RemainingUses = 1
|
||||
if inv.RemainingUses != 0 {
|
||||
invite.RemainingUses = inv.RemainingUses
|
||||
}
|
||||
if inv.SendTo != "" {
|
||||
invite.SendTo = inv.SendTo
|
||||
}
|
||||
if len(inv.Notify) != 0 {
|
||||
// app.err.Printf("%s has notify section: %+v, you are %s\n", inv.Code, inv.Notify, gc.GetString("jfId"))
|
||||
var addressOrID string
|
||||
if app.config.Section("ui").Key("jellyfin_login").MustBool(false) {
|
||||
addressOrID = gc.GetString("jfId")
|
||||
} else {
|
||||
addressOrID = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
if _, ok := inv.Notify[addressOrID]; ok {
|
||||
if _, ok = inv.Notify[addressOrID]["notify-expiry"]; ok {
|
||||
invite.NotifyExpiry = inv.Notify[addressOrID]["notify-expiry"]
|
||||
}
|
||||
if _, ok = inv.Notify[addressOrID]["notify-creation"]; ok {
|
||||
invite.NotifyCreation = inv.Notify[addressOrID]["notify-creation"]
|
||||
}
|
||||
}
|
||||
}
|
||||
invites = append(invites, invite)
|
||||
}
|
||||
fullProfileList := app.storage.GetProfiles()
|
||||
profiles := make([]string, len(fullProfileList))
|
||||
if len(profiles) != 0 {
|
||||
defaultProfile := app.storage.GetDefaultProfile()
|
||||
profiles[0] = defaultProfile.Name
|
||||
i := 1
|
||||
if len(fullProfileList) > 1 {
|
||||
app.storage.db.ForEach(badgerhold.Where("Name").Ne(profiles[0]), func(p *Profile) error {
|
||||
profiles[i] = p.Name
|
||||
i++
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
resp := getInvitesDTO{
|
||||
Profiles: profiles,
|
||||
Invites: invites,
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Set profile for an invite
|
||||
// @Produce json
|
||||
// @Param inviteProfileDTO body inviteProfileDTO true "Invite profile object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /invites/profile [post]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) SetProfile(gc *gin.Context) {
|
||||
var req inviteProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Setting profile to \"%s\"", req.Invite, req.Profile)
|
||||
// "" means "Don't apply profile"
|
||||
if _, ok := app.storage.GetProfileKey(req.Profile); !ok && req.Profile != "" {
|
||||
app.err.Printf("%s: Profile \"%s\" not found", req.Invite, req.Profile)
|
||||
respond(500, "Profile not found", gc)
|
||||
return
|
||||
}
|
||||
inv, _ := app.storage.GetInvitesKey(req.Invite)
|
||||
inv.Profile = req.Profile
|
||||
app.storage.SetInvitesKey(req.Invite, inv)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Set notification preferences for an invite.
|
||||
// @Produce json
|
||||
// @Param setNotifyDTO body setNotifyDTO true "Map of invite codes to notification settings objects"
|
||||
// @Success 200
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /invites/notify [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) SetNotify(gc *gin.Context) {
|
||||
var req map[string]map[string]bool
|
||||
gc.BindJSON(&req)
|
||||
changed := false
|
||||
for code, settings := range req {
|
||||
app.debug.Printf("%s: Notification settings change requested", code)
|
||||
invite, ok := app.storage.GetInvitesKey(code)
|
||||
if !ok {
|
||||
app.err.Printf("%s Notification setting change failed: Invalid code", code)
|
||||
respond(400, "Invalid invite code", gc)
|
||||
return
|
||||
}
|
||||
var address string
|
||||
jellyfinLogin := app.config.Section("ui").Key("jellyfin_login").MustBool(false)
|
||||
if jellyfinLogin {
|
||||
var addressAvailable bool = app.getAddressOrName(gc.GetString("jfId")) != ""
|
||||
if !addressAvailable {
|
||||
app.err.Printf("%s: Couldn't find contact method for admin. Make sure one is set.", code)
|
||||
app.debug.Printf("%s: User ID \"%s\"", code, gc.GetString("jfId"))
|
||||
respond(500, "Missing user contact method", gc)
|
||||
return
|
||||
}
|
||||
address = gc.GetString("jfId")
|
||||
} else {
|
||||
address = app.config.Section("ui").Key("email").String()
|
||||
}
|
||||
if invite.Notify == nil {
|
||||
invite.Notify = map[string]map[string]bool{}
|
||||
}
|
||||
if _, ok := invite.Notify[address]; !ok {
|
||||
invite.Notify[address] = map[string]bool{}
|
||||
} /*else {
|
||||
if _, ok := invite.Notify[address]["notify-expiry"]; !ok {
|
||||
*/
|
||||
if _, ok := settings["notify-expiry"]; ok && invite.Notify[address]["notify-expiry"] != settings["notify-expiry"] {
|
||||
invite.Notify[address]["notify-expiry"] = settings["notify-expiry"]
|
||||
app.debug.Printf("%s: Set \"notify-expiry\" to %t for %s", code, settings["notify-expiry"], address)
|
||||
changed = true
|
||||
}
|
||||
if _, ok := settings["notify-creation"]; ok && invite.Notify[address]["notify-creation"] != settings["notify-creation"] {
|
||||
invite.Notify[address]["notify-creation"] = settings["notify-creation"]
|
||||
app.debug.Printf("%s: Set \"notify-creation\" to %t for %s", code, settings["notify-creation"], address)
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
app.storage.SetInvitesKey(code, invite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Delete an invite.
|
||||
// @Produce json
|
||||
// @Param deleteInviteDTO body deleteInviteDTO true "Delete invite object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Router /invites [delete]
|
||||
// @Security Bearer
|
||||
// @tags Invites
|
||||
func (app *appContext) DeleteInvite(gc *gin.Context) {
|
||||
var req deleteInviteDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Printf("%s: Deletion requested", req.Code)
|
||||
var ok bool
|
||||
_, ok = app.storage.GetInvitesKey(req.Code)
|
||||
if ok {
|
||||
app.storage.DeleteInvitesKey(req.Code)
|
||||
app.info.Printf("%s: Invite deleted", req.Code)
|
||||
respondBool(200, true, gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("%s: Deletion failed: Invalid code", req.Code)
|
||||
respond(400, "Code doesn't exist", gc)
|
||||
}
|
||||
737
api-messages.go
Normal file
@@ -0,0 +1,737 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// @Summary Get a list of email names and IDs.
|
||||
// @Produce json
|
||||
// @Param lang query string false "Language for email titles."
|
||||
// @Success 200 {object} emailListDTO
|
||||
// @Router /config/emails [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetCustomContent(gc *gin.Context) {
|
||||
lang := gc.Query("lang")
|
||||
if _, ok := app.storage.lang.Email[lang]; !ok {
|
||||
lang = app.storage.lang.chosenEmailLang
|
||||
}
|
||||
adminLang := lang
|
||||
if _, ok := app.storage.lang.Admin[lang]; !ok {
|
||||
adminLang = app.storage.lang.chosenAdminLang
|
||||
}
|
||||
list := emailListDTO{
|
||||
"UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.MustGetCustomContentKey("UserCreated").Enabled},
|
||||
"InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.MustGetCustomContentKey("InviteExpiry").Enabled},
|
||||
"PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.MustGetCustomContentKey("PasswordReset").Enabled},
|
||||
"UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled},
|
||||
"UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled},
|
||||
"UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled},
|
||||
"InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled},
|
||||
"WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled},
|
||||
"EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled},
|
||||
"UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled},
|
||||
"UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("Login").Enabled},
|
||||
"UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("Page").Enabled},
|
||||
}
|
||||
|
||||
filter := gc.Query("filter")
|
||||
if filter == "user" {
|
||||
list = emailListDTO{"UserLogin": list["UserLogin"], "UserPage": list["UserPage"]}
|
||||
} else {
|
||||
delete(list, "UserLogin")
|
||||
delete(list, "UserPage")
|
||||
}
|
||||
|
||||
gc.JSON(200, list)
|
||||
}
|
||||
|
||||
// No longer needed, these are stored by string keys in the database now.
|
||||
/* func (app *appContext) getCustomMessage(id string) *CustomContent {
|
||||
switch id {
|
||||
case "Announcement":
|
||||
return &CustomContent{}
|
||||
case "UserCreated":
|
||||
return &app.storage.customEmails.UserCreated
|
||||
case "InviteExpiry":
|
||||
return &app.storage.customEmails.InviteExpiry
|
||||
case "PasswordReset":
|
||||
return &app.storage.customEmails.PasswordReset
|
||||
case "UserDeleted":
|
||||
return &app.storage.customEmails.UserDeleted
|
||||
case "UserDisabled":
|
||||
return &app.storage.customEmails.UserDisabled
|
||||
case "UserEnabled":
|
||||
return &app.storage.customEmails.UserEnabled
|
||||
case "InviteEmail":
|
||||
return &app.storage.customEmails.InviteEmail
|
||||
case "WelcomeEmail":
|
||||
return &app.storage.customEmails.WelcomeEmail
|
||||
case "EmailConfirmation":
|
||||
return &app.storage.customEmails.EmailConfirmation
|
||||
case "UserExpired":
|
||||
return &app.storage.customEmails.UserExpired
|
||||
case "UserLogin":
|
||||
return &app.storage.userPage.Login
|
||||
case "UserPage":
|
||||
return &app.storage.userPage.Page
|
||||
}
|
||||
return nil
|
||||
} */
|
||||
|
||||
// @Summary Sets the corresponding custom content.
|
||||
// @Produce json
|
||||
// @Param CustomContent body CustomContent true "Content = email (in markdown)."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param id path string true "ID of content"
|
||||
// @Router /config/emails/{id} [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetCustomMessage(gc *gin.Context) {
|
||||
var req CustomContent
|
||||
gc.BindJSON(&req)
|
||||
id := gc.Param("id")
|
||||
if req.Content == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
message, ok := app.storage.GetCustomContentKey(id)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
message.Content = req.Content
|
||||
message.Enabled = true
|
||||
app.storage.SetCustomContentKey(id, message)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Enable/Disable custom content.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param enable/disable path string true "enable/disable"
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id}/state/{enable/disable} [post]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) SetCustomMessageState(gc *gin.Context) {
|
||||
id := gc.Param("id")
|
||||
s := gc.Param("state")
|
||||
enabled := false
|
||||
if s == "enable" {
|
||||
enabled = true
|
||||
} else if s != "disable" {
|
||||
respondBool(400, false, gc)
|
||||
}
|
||||
message, ok := app.storage.GetCustomContentKey(id)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
message.Enabled = enabled
|
||||
app.storage.SetCustomContentKey(id, message)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns the custom content/message (generating it if not set) and list of used variables in it.
|
||||
// @Produce json
|
||||
// @Success 200 {object} customEmailDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param id path string true "ID of email"
|
||||
// @Router /config/emails/{id} [get]
|
||||
// @Security Bearer
|
||||
// @tags Configuration
|
||||
func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
|
||||
lang := app.storage.lang.chosenEmailLang
|
||||
id := gc.Param("id")
|
||||
var content string
|
||||
var err error
|
||||
var msg *Message
|
||||
var variables []string
|
||||
var conditionals []string
|
||||
var values map[string]interface{}
|
||||
username := app.storage.lang.Email[lang].Strings.get("username")
|
||||
emailAddress := app.storage.lang.Email[lang].Strings.get("emailAddress")
|
||||
customMessage, ok := app.storage.GetCustomContentKey(id)
|
||||
if !ok {
|
||||
app.err.Printf("Failed to get custom message with ID \"%s\"", id)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
if id == "WelcomeEmail" {
|
||||
conditionals = []string{"{yourAccountWillExpire}"}
|
||||
customMessage.Conditionals = conditionals
|
||||
} else if id == "UserPage" {
|
||||
variables = []string{"{username}"}
|
||||
customMessage.Variables = variables
|
||||
} else if id == "UserLogin" {
|
||||
variables = []string{}
|
||||
customMessage.Variables = variables
|
||||
}
|
||||
content = customMessage.Content
|
||||
noContent := content == ""
|
||||
if !noContent {
|
||||
variables = customMessage.Variables
|
||||
}
|
||||
switch id {
|
||||
case "Announcement":
|
||||
// Just send the email html
|
||||
content = ""
|
||||
case "UserCreated":
|
||||
if noContent {
|
||||
msg, err = app.email.constructCreated("", "", "", Invite{}, app, true)
|
||||
}
|
||||
values = app.email.createdValues("xxxxxx", username, emailAddress, Invite{}, app, false)
|
||||
case "InviteExpiry":
|
||||
if noContent {
|
||||
msg, err = app.email.constructExpiry("", Invite{}, app, true)
|
||||
}
|
||||
values = app.email.expiryValues("xxxxxx", Invite{}, app, false)
|
||||
case "PasswordReset":
|
||||
if noContent {
|
||||
msg, err = app.email.constructReset(PasswordReset{}, app, true)
|
||||
}
|
||||
values = app.email.resetValues(PasswordReset{Pin: "12-34-56", Username: username}, app, false)
|
||||
case "UserDeleted":
|
||||
if noContent {
|
||||
msg, err = app.email.constructDeleted("", app, true)
|
||||
}
|
||||
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
|
||||
case "UserDisabled":
|
||||
if noContent {
|
||||
msg, err = app.email.constructDisabled("", app, true)
|
||||
}
|
||||
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
|
||||
case "UserEnabled":
|
||||
if noContent {
|
||||
msg, err = app.email.constructEnabled("", app, true)
|
||||
}
|
||||
values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false)
|
||||
case "InviteEmail":
|
||||
if noContent {
|
||||
msg, err = app.email.constructInvite("", Invite{}, app, true)
|
||||
}
|
||||
values = app.email.inviteValues("xxxxxx", Invite{}, app, false)
|
||||
case "WelcomeEmail":
|
||||
if noContent {
|
||||
msg, err = app.email.constructWelcome("", time.Time{}, app, true)
|
||||
}
|
||||
values = app.email.welcomeValues(username, time.Now(), app, false, true)
|
||||
case "EmailConfirmation":
|
||||
if noContent {
|
||||
msg, err = app.email.constructConfirmation("", "", "", app, true)
|
||||
}
|
||||
values = app.email.confirmationValues("xxxxxx", username, "xxxxxx", app, false)
|
||||
case "UserExpired":
|
||||
if noContent {
|
||||
msg, err = app.email.constructUserExpired(app, true)
|
||||
}
|
||||
values = app.email.userExpiredValues(app, false)
|
||||
case "UserLogin", "UserPage":
|
||||
values = map[string]interface{}{}
|
||||
}
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if noContent && id != "Announcement" && id != "UserPage" && id != "UserLogin" {
|
||||
content = msg.Text
|
||||
variables = make([]string, strings.Count(content, "{"))
|
||||
i := 0
|
||||
found := false
|
||||
buf := ""
|
||||
for _, c := range content {
|
||||
if !found && c != '{' && c != '}' {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
buf += string(c)
|
||||
if c == '}' {
|
||||
found = false
|
||||
variables[i] = buf
|
||||
buf = ""
|
||||
i++
|
||||
}
|
||||
}
|
||||
customMessage.Variables = variables
|
||||
}
|
||||
if variables == nil {
|
||||
variables = []string{}
|
||||
}
|
||||
app.storage.SetCustomContentKey(id, customMessage)
|
||||
var mail *Message
|
||||
if id != "UserLogin" && id != "UserPage" {
|
||||
mail, err = app.email.constructTemplate("", "<div class=\"preview-content\"></div>", app)
|
||||
if err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mail = &Message{
|
||||
HTML: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
|
||||
Markdown: "<div class=\"card ~neutral dark:~d_neutral @low preview-content\"></div>",
|
||||
}
|
||||
}
|
||||
gc.JSON(200, customEmailDTO{Content: content, Variables: variables, Conditionals: conditionals, Values: values, HTML: mail.HTML, Plaintext: mail.Text})
|
||||
}
|
||||
|
||||
// @Summary Returns a new Telegram verification PIN, and the bot username.
|
||||
// @Produce json
|
||||
// @Success 200 {object} telegramPinDTO
|
||||
// @Router /telegram/pin [get]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramGetPin(gc *gin.Context) {
|
||||
gc.JSON(200, telegramPinDTO{
|
||||
Token: app.telegram.NewAuthToken(),
|
||||
Username: app.telegram.username,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Link a Jellyfin & Telegram user together via a verification PIN.
|
||||
// @Produce json
|
||||
// @Param telegramSetDTO body telegramSetDTO true "Token and user's Jellyfin ID."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Router /users/telegram [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramAddUser(gc *gin.Context) {
|
||||
var req telegramSetDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.Token == "" || req.ID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
tgToken, ok := app.telegram.TokenVerified(req.Token)
|
||||
app.telegram.DeleteVerifiedToken(req.Token)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
tgUser := TelegramUser{
|
||||
ChatID: tgToken.ChatID,
|
||||
Username: tgToken.Username,
|
||||
Contact: true,
|
||||
}
|
||||
if lang, ok := app.telegram.languages[tgToken.ChatID]; ok {
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Sets whether to notify a user through telegram/discord/matrix/email or not.
|
||||
// @Produce json
|
||||
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 400 {object} boolResponse
|
||||
// @Success 500 {object} boolResponse
|
||||
// @Router /users/contact [post]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) SetContactMethods(gc *gin.Context) {
|
||||
var req SetContactMethodsDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.ID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
app.setContactMethods(req, gc)
|
||||
}
|
||||
|
||||
func (app *appContext) setContactMethods(req SetContactMethodsDTO, gc *gin.Context) {
|
||||
if tgUser, ok := app.storage.GetTelegramKey(req.ID); ok {
|
||||
change := tgUser.Contact != req.Telegram
|
||||
tgUser.Contact = req.Telegram
|
||||
app.storage.SetTelegramKey(req.ID, tgUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Telegram {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Telegram: User \"%s\" will%s be notified through Telegram.", tgUser.Username, msg)
|
||||
}
|
||||
}
|
||||
if dcUser, ok := app.storage.GetDiscordKey(req.ID); ok {
|
||||
change := dcUser.Contact != req.Discord
|
||||
dcUser.Contact = req.Discord
|
||||
app.storage.SetDiscordKey(req.ID, dcUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Discord {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Discord: User \"%s\" will%s be notified through Discord.", dcUser.Username, msg)
|
||||
}
|
||||
}
|
||||
if mxUser, ok := app.storage.GetMatrixKey(req.ID); ok {
|
||||
change := mxUser.Contact != req.Matrix
|
||||
mxUser.Contact = req.Matrix
|
||||
app.storage.SetMatrixKey(req.ID, mxUser)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Matrix {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("Matrix: User \"%s\" will%s be notified through Matrix.", mxUser.UserID, msg)
|
||||
}
|
||||
}
|
||||
if email, ok := app.storage.GetEmailsKey(req.ID); ok {
|
||||
change := email.Contact != req.Email
|
||||
email.Contact = req.Email
|
||||
app.storage.SetEmailsKey(req.ID, email)
|
||||
if change {
|
||||
msg := ""
|
||||
if !req.Email {
|
||||
msg = " not"
|
||||
}
|
||||
app.debug.Printf("\"%s\" will%s be notified via Email.", email.Addr, msg)
|
||||
}
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires bearer auth.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Router /telegram/verified/{pin} [get]
|
||||
// @Security Bearer
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramVerified(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
_, ok := app.telegram.TokenVerified(pin)
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a telegram PIN was verified. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /invite/{invCode}/telegram/verified/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) TelegramVerifiedInvite(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
token, ok := app.telegram.TokenVerified(pin)
|
||||
if ok && app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
||||
app.discord.DeleteVerifiedUser(pin)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not a discord PIN was verified. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /invite/{invCode}/discord/verified/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordVerifiedInvite(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.discord.UserVerified(pin)
|
||||
if ok && app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(user.ID) {
|
||||
delete(app.discord.verifiedTokens, pin)
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, ok, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a 10-minute, one-use Discord server invite
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordInviteDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /invite/{invCode}/discord/invite [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
|
||||
if invURL == "" {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
||||
}
|
||||
|
||||
// @Summary Generate and send a new PIN to a specified Matrix user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
||||
// @Router /invite/{invCode}/matrix/user [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixSendPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
var req MatrixSendPINDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.UserID == "" {
|
||||
respond(400, "errorNoUserID", gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.GetMatrix() {
|
||||
if req.UserID == u.UserID {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok := app.matrix.SendStart(req.UserID)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Check whether a matrix PIN is valid, and mark the token as verified if so. Requires invite code.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param userID path string true "Matrix User ID"
|
||||
// @Router /invite/{invCode}/matrix/verified/{userID}/{pin} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixCheckPIN(gc *gin.Context) {
|
||||
code := gc.Param("invCode")
|
||||
if _, ok := app.storage.GetInvitesKey(code); !ok {
|
||||
app.debug.Println("Matrix: Invite code was invalid")
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
userID := gc.Param("userID")
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if user.User.UserID != userID {
|
||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
user.Verified = true
|
||||
app.matrix.tokens[pin] = user
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Generates a Matrix access token from a username and password.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixLoginDTO body MatrixLoginDTO true "Username & password."
|
||||
// @Router /matrix/login [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixLogin(gc *gin.Context) {
|
||||
var req MatrixLoginDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.Username == "" || req.Password == "" {
|
||||
respond(400, "errorLoginBlank", gc)
|
||||
return
|
||||
}
|
||||
token, err := app.matrix.generateAccessToken(req.Homeserver, req.Username, req.Password)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to generate token: %v", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
tempConfig, _ := ini.Load(app.configPath)
|
||||
matrix := tempConfig.Section("matrix")
|
||||
matrix.Key("enabled").SetValue("true")
|
||||
matrix.Key("homeserver").SetValue(req.Homeserver)
|
||||
matrix.Key("token").SetValue(token)
|
||||
matrix.Key("user_id").SetValue(req.Username)
|
||||
if err := tempConfig.SaveTo(app.configPath); err != nil {
|
||||
app.err.Printf("Failed to save config to \"%s\": %v", app.configPath, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Links a Matrix user to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixConnectUserDTO body MatrixConnectUserDTO true "User's Jellyfin ID & Matrix user ID."
|
||||
// @Router /users/matrix [post]
|
||||
// @tags Other
|
||||
func (app *appContext) MatrixConnect(gc *gin.Context) {
|
||||
var req MatrixConnectUserDTO
|
||||
gc.BindJSON(&req)
|
||||
if app.storage.GetMatrix() == nil {
|
||||
app.storage.deprecatedMatrix = matrixStore{}
|
||||
}
|
||||
roomID, encrypted, err := app.matrix.CreateRoom(req.UserID)
|
||||
if err != nil {
|
||||
app.err.Printf("Matrix: Failed to create room: %v", err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.SetMatrixKey(req.JellyfinID, MatrixUser{
|
||||
UserID: req.UserID,
|
||||
RoomID: string(roomID),
|
||||
Lang: "en-us",
|
||||
Contact: true,
|
||||
Encrypted: encrypted,
|
||||
})
|
||||
app.matrix.isEncrypted[roomID] = encrypted
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns a list of matching users from a Discord guild, given a username (discriminator optional).
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordUsersDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param username path string true "username to search."
|
||||
// @Router /users/discord/{username} [get]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordGetUsers(gc *gin.Context) {
|
||||
name := gc.Param("username")
|
||||
if name == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
users := app.discord.GetUsers(name)
|
||||
resp := DiscordUsersDTO{Users: make([]DiscordUserDTO, len(users))}
|
||||
for i, u := range users {
|
||||
resp.Users[i] = DiscordUserDTO{
|
||||
Name: RenderDiscordUsername(u.User),
|
||||
ID: u.User.ID,
|
||||
AvatarURL: u.User.AvatarURL("32"),
|
||||
}
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Links a Discord account to a Jellyfin account via user IDs. Notifications are turned on by default.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param DiscordConnectUserDTO body DiscordConnectUserDTO true "User's Jellyfin ID & Discord ID."
|
||||
// @Router /users/discord [post]
|
||||
// @tags Other
|
||||
func (app *appContext) DiscordConnect(gc *gin.Context) {
|
||||
var req DiscordConnectUserDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.JellyfinID == "" || req.DiscordID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
user, ok := app.discord.NewUser(req.DiscordID)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
app.storage.SetDiscordKey(req.JellyfinID, user)
|
||||
linkExistingOmbiDiscordTelegram(app)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary unlink a Discord account from a Jellyfin user. Always succeeds.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
|
||||
// @Router /users/discord [delete]
|
||||
// @Tags Users
|
||||
func (app *appContext) UnlinkDiscord(gc *gin.Context) {
|
||||
var req forUserDTO
|
||||
gc.BindJSON(&req)
|
||||
/* user, status, err := app.jf.UserByID(req.ID, false)
|
||||
if req.ID == "" || status != 200 || err != nil {
|
||||
respond(400, "User not found", gc)
|
||||
return
|
||||
} */
|
||||
app.storage.DeleteDiscordKey(req.ID)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary unlink a Telegram account from a Jellyfin user. Always succeeds.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
|
||||
// @Router /users/telegram [delete]
|
||||
// @Tags Users
|
||||
func (app *appContext) UnlinkTelegram(gc *gin.Context) {
|
||||
var req forUserDTO
|
||||
gc.BindJSON(&req)
|
||||
/* user, status, err := app.jf.UserByID(req.ID, false)
|
||||
if req.ID == "" || status != 200 || err != nil {
|
||||
respond(400, "User not found", gc)
|
||||
return
|
||||
} */
|
||||
app.storage.DeleteTelegramKey(req.ID)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary unlink a Matrix account from a Jellyfin user. Always succeeds.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
|
||||
// @Router /users/matrix [delete]
|
||||
// @Tags Users
|
||||
func (app *appContext) UnlinkMatrix(gc *gin.Context) {
|
||||
var req forUserDTO
|
||||
gc.BindJSON(&req)
|
||||
/* user, status, err := app.jf.UserByID(req.ID, false)
|
||||
if req.ID == "" || status != 200 || err != nil {
|
||||
respond(400, "User not found", gc)
|
||||
return
|
||||
} */
|
||||
app.storage.DeleteMatrixKey(req.ID)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
109
api-ombi.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (app *appContext) getOmbiUser(jfID string) (map[string]interface{}, int, error) {
|
||||
ombiUsers, code, err := app.ombi.GetUsers()
|
||||
if err != nil || code != 200 {
|
||||
return nil, code, err
|
||||
}
|
||||
jfUser, code, err := app.jf.UserByID(jfID, false)
|
||||
if err != nil || code != 200 {
|
||||
return nil, code, err
|
||||
}
|
||||
username := jfUser.Name
|
||||
email := ""
|
||||
if e, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||
email = e.Addr
|
||||
}
|
||||
for _, ombiUser := range ombiUsers {
|
||||
ombiAddr := ""
|
||||
if a, ok := ombiUser["emailAddress"]; ok && a != nil {
|
||||
ombiAddr = a.(string)
|
||||
}
|
||||
if ombiUser["userName"].(string) == username || (ombiAddr == email && email != "") {
|
||||
return ombiUser, code, err
|
||||
}
|
||||
}
|
||||
return nil, 400, fmt.Errorf("Couldn't find user")
|
||||
}
|
||||
|
||||
// @Summary Get a list of Ombi users.
|
||||
// @Produce json
|
||||
// @Success 200 {object} ombiUsersDTO
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /ombi/users [get]
|
||||
// @Security Bearer
|
||||
// @tags Ombi
|
||||
func (app *appContext) OmbiUsers(gc *gin.Context) {
|
||||
app.debug.Println("Ombi users requested")
|
||||
users, status, err := app.ombi.GetUsers()
|
||||
if err != nil || status != 200 {
|
||||
app.err.Printf("Failed to get users from Ombi (%d): %v", status, err)
|
||||
respond(500, "Couldn't get users", gc)
|
||||
return
|
||||
}
|
||||
userlist := make([]ombiUser, len(users))
|
||||
for i, data := range users {
|
||||
userlist[i] = ombiUser{
|
||||
Name: data["userName"].(string),
|
||||
ID: data["id"].(string),
|
||||
}
|
||||
}
|
||||
gc.JSON(200, ombiUsersDTO{Users: userlist})
|
||||
}
|
||||
|
||||
// @Summary Store Ombi user template in an existing profile.
|
||||
// @Produce json
|
||||
// @Param ombiUser body ombiUser true "User to source settings from"
|
||||
// @Param profile path string true "Name of profile to store in"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles/ombi/{profile} [post]
|
||||
// @Security Bearer
|
||||
// @tags Ombi
|
||||
func (app *appContext) SetOmbiProfile(gc *gin.Context) {
|
||||
var req ombiUser
|
||||
gc.BindJSON(&req)
|
||||
profileName := gc.Param("profile")
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
template, code, err := app.ombi.TemplateByID(req.ID)
|
||||
if err != nil || code != 200 || len(template) == 0 {
|
||||
app.err.Printf("Couldn't get user from Ombi (%d): %v", code, err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
profile.Ombi = template
|
||||
app.storage.SetProfileKey(profileName, profile)
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Remove ombi user template from a profile.
|
||||
// @Produce json
|
||||
// @Param profile path string true "Name of profile to store in"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles/ombi/{profile} [delete]
|
||||
// @Security Bearer
|
||||
// @tags Ombi
|
||||
func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
|
||||
profileName := gc.Param("profile")
|
||||
profile, ok := app.storage.GetProfileKey(profileName)
|
||||
if !ok {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
profile.Ombi = nil
|
||||
app.storage.SetProfileKey(profileName, profile)
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
112
api-profiles.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
// @Summary Get a list of profiles
|
||||
// @Produce json
|
||||
// @Success 200 {object} getProfilesDTO
|
||||
// @Router /profiles [get]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) GetProfiles(gc *gin.Context) {
|
||||
app.debug.Println("Profiles requested")
|
||||
out := getProfilesDTO{
|
||||
DefaultProfile: app.storage.GetDefaultProfile().Name,
|
||||
Profiles: map[string]profileDTO{},
|
||||
}
|
||||
for _, p := range app.storage.GetProfiles() {
|
||||
out.Profiles[p.Name] = profileDTO{
|
||||
Admin: p.Admin,
|
||||
LibraryAccess: p.LibraryAccess,
|
||||
FromUser: p.FromUser,
|
||||
Ombi: p.Ombi != nil,
|
||||
}
|
||||
}
|
||||
gc.JSON(200, out)
|
||||
}
|
||||
|
||||
// @Summary Set the default profile to use.
|
||||
// @Produce json
|
||||
// @Param profileChangeDTO body profileChangeDTO true "Default profile object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles/default [post]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) SetDefaultProfile(gc *gin.Context) {
|
||||
req := profileChangeDTO{}
|
||||
gc.BindJSON(&req)
|
||||
app.info.Printf("Setting default profile to \"%s\"", req.Name)
|
||||
if _, ok := app.storage.GetProfileKey(req.Name); !ok {
|
||||
app.err.Printf("Profile not found: \"%s\"", req.Name)
|
||||
respond(500, "Profile not found", gc)
|
||||
return
|
||||
}
|
||||
app.storage.db.ForEach(&badgerhold.Query{}, func(profile *Profile) error {
|
||||
if profile.Name == req.Name {
|
||||
profile.Default = true
|
||||
} else {
|
||||
profile.Default = false
|
||||
}
|
||||
app.storage.SetProfileKey(profile.Name, *profile)
|
||||
return nil
|
||||
})
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Create a profile based on a Jellyfin user's settings.
|
||||
// @Produce json
|
||||
// @Param newProfileDTO body newProfileDTO true "New profile object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /profiles [post]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) CreateProfile(gc *gin.Context) {
|
||||
app.info.Println("Profile creation requested")
|
||||
var req newProfileDTO
|
||||
gc.BindJSON(&req)
|
||||
app.jf.CacheExpiry = time.Now()
|
||||
user, status, err := app.jf.UserByID(req.ID, false)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin (%d): %v", status, err)
|
||||
respond(500, "Couldn't get user", gc)
|
||||
return
|
||||
}
|
||||
profile := Profile{
|
||||
FromUser: user.Name,
|
||||
Policy: user.Policy,
|
||||
}
|
||||
app.debug.Printf("Creating profile from user \"%s\"", user.Name)
|
||||
if req.Homescreen {
|
||||
profile.Configuration = user.Configuration
|
||||
profile.Displayprefs, status, err = app.jf.GetDisplayPreferences(req.ID)
|
||||
if !(status == 200 || status == 204) || err != nil {
|
||||
app.err.Printf("Failed to get DisplayPrefs (%d): %v", status, err)
|
||||
respond(500, "Couldn't get displayprefs", gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
app.storage.SetProfileKey(req.Name, profile)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Delete an existing profile
|
||||
// @Produce json
|
||||
// @Param profileChangeDTO body profileChangeDTO true "Delete profile object"
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Router /profiles [delete]
|
||||
// @Security Bearer
|
||||
// @tags Profiles & Settings
|
||||
func (app *appContext) DeleteProfile(gc *gin.Context) {
|
||||
req := profileChangeDTO{}
|
||||
gc.BindJSON(&req)
|
||||
name := req.Name
|
||||
app.storage.DeleteProfileKey(name)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
623
api-userpage.go
Normal file
@@ -0,0 +1,623 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
// @Summary Returns the logged-in user's Jellyfin ID & Username, and other details.
|
||||
// @Produce json
|
||||
// @Success 200 {object} MyDetailsDTO
|
||||
// @Router /my/details [get]
|
||||
// @tags User Page
|
||||
func (app *appContext) MyDetails(gc *gin.Context) {
|
||||
resp := MyDetailsDTO{
|
||||
Id: gc.GetString("jfId"),
|
||||
}
|
||||
|
||||
user, status, err := app.jf.UserByID(resp.Id, false)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get Jellyfin user (%d): %+v\n", status, err)
|
||||
respond(500, "Failed to get user", gc)
|
||||
return
|
||||
}
|
||||
resp.Username = user.Name
|
||||
resp.Admin = user.Policy.IsAdministrator
|
||||
resp.AccountsAdmin = false
|
||||
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
|
||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||
if emailStore, ok := app.storage.GetEmailsKey(resp.Id); ok {
|
||||
resp.AccountsAdmin = emailStore.Admin
|
||||
}
|
||||
resp.AccountsAdmin = resp.AccountsAdmin || (adminOnly && resp.Admin)
|
||||
}
|
||||
resp.Disabled = user.Policy.IsDisabled
|
||||
|
||||
if exp, ok := app.storage.GetUserExpiryKey(user.ID); ok {
|
||||
resp.Expiry = exp.Expiry.Unix()
|
||||
}
|
||||
|
||||
if emailEnabled {
|
||||
resp.Email = &MyDetailsContactMethodsDTO{}
|
||||
if email, ok := app.storage.GetEmailsKey(user.ID); ok && email.Addr != "" {
|
||||
resp.Email.Value = email.Addr
|
||||
resp.Email.Enabled = email.Contact
|
||||
}
|
||||
}
|
||||
|
||||
if discordEnabled {
|
||||
resp.Discord = &MyDetailsContactMethodsDTO{}
|
||||
if discord, ok := app.storage.GetDiscordKey(user.ID); ok {
|
||||
resp.Discord.Value = RenderDiscordUsername(discord)
|
||||
resp.Discord.Enabled = discord.Contact
|
||||
}
|
||||
}
|
||||
|
||||
if telegramEnabled {
|
||||
resp.Telegram = &MyDetailsContactMethodsDTO{}
|
||||
if telegram, ok := app.storage.GetTelegramKey(user.ID); ok {
|
||||
resp.Telegram.Value = telegram.Username
|
||||
resp.Telegram.Enabled = telegram.Contact
|
||||
}
|
||||
}
|
||||
|
||||
if matrixEnabled {
|
||||
resp.Matrix = &MyDetailsContactMethodsDTO{}
|
||||
if matrix, ok := app.storage.GetMatrixKey(user.ID); ok {
|
||||
resp.Matrix.Value = matrix.UserID
|
||||
resp.Matrix.Enabled = matrix.Contact
|
||||
}
|
||||
}
|
||||
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Sets whether to notify yourself through telegram/discord/matrix/email or not.
|
||||
// @Produce json
|
||||
// @Param SetContactMethodsDTO body SetContactMethodsDTO true "User's Jellyfin ID and whether or not to notify then through Telegram."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Success 400 {object} boolResponse
|
||||
// @Success 500 {object} boolResponse
|
||||
// @Router /my/contact [post]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) SetMyContactMethods(gc *gin.Context) {
|
||||
var req SetContactMethodsDTO
|
||||
gc.BindJSON(&req)
|
||||
req.ID = gc.GetString("jfId")
|
||||
if req.ID == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
app.setContactMethods(req, gc)
|
||||
}
|
||||
|
||||
// @Summary Logout by deleting refresh token from cookies.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /my/logout [post]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) LogoutUser(gc *gin.Context) {
|
||||
cookie, err := gc.Cookie("user-refresh")
|
||||
if err != nil {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
respond(500, "Couldn't fetch cookies", gc)
|
||||
return
|
||||
}
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary confirm an action (e.g. changing an email address.)
|
||||
// @Produce json
|
||||
// @Param jwt path string true "jwt confirmation code"
|
||||
// @Router /my/confirm/{jwt} [post]
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 404
|
||||
// @Success 303
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @tags User Page
|
||||
func (app *appContext) ConfirmMyAction(gc *gin.Context) {
|
||||
app.confirmMyAction(gc, "")
|
||||
}
|
||||
|
||||
func (app *appContext) confirmMyAction(gc *gin.Context, key string) {
|
||||
var claims jwt.MapClaims
|
||||
var target ConfirmationTarget
|
||||
var id string
|
||||
fail := func() {
|
||||
gcHTML(gc, 404, "404.html", gin.H{
|
||||
"cssClass": app.cssClass,
|
||||
"cssVersion": cssVersion,
|
||||
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate key
|
||||
if key == "" {
|
||||
key = gc.Param("jwt")
|
||||
}
|
||||
token, err := jwt.Parse(key, checkToken)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
fail()
|
||||
// respond(500, "unknownError", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
app.err.Printf("Failed to parse key: %s", err)
|
||||
fail()
|
||||
// respond(500, "unknownError", gc)
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(int64(claims["exp"].(float64)), 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "confirmation" && expiry.After(time.Now())) {
|
||||
app.err.Printf("Invalid key")
|
||||
fail()
|
||||
// respond(400, "invalidKey", gc)
|
||||
return
|
||||
}
|
||||
target = ConfirmationTarget(int(claims["target"].(float64)))
|
||||
id = claims["id"].(string)
|
||||
|
||||
// Perform an Action
|
||||
if target == NoOp {
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
return
|
||||
} else if target == UserEmailChange {
|
||||
emailStore, ok := app.storage.GetEmailsKey(id)
|
||||
if !ok {
|
||||
emailStore = EmailAddress{
|
||||
Contact: true,
|
||||
}
|
||||
}
|
||||
emailStore.Addr = claims["email"].(string)
|
||||
app.storage.SetEmailsKey(id, emailStore)
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
ombiUser, code, err := app.getOmbiUser(id)
|
||||
if code == 200 && err == nil {
|
||||
ombiUser["emailAddress"] = claims["email"].(string)
|
||||
code, err = app.ombi.ModifyUser(ombiUser)
|
||||
if code != 200 || err != nil {
|
||||
app.err.Printf("%s: Failed to change ombi email address (%d): %v", ombiUser["userName"].(string), code, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.info.Println("Email list modified")
|
||||
gc.Redirect(http.StatusSeeOther, "/my/account")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Modify your email address.
|
||||
// @Produce json
|
||||
// @Param ModifyMyEmailDTO body ModifyMyEmailDTO true "New email address."
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} stringResponse
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /my/email [post]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) ModifyMyEmail(gc *gin.Context) {
|
||||
var req ModifyMyEmailDTO
|
||||
gc.BindJSON(&req)
|
||||
app.debug.Println("Email modification requested")
|
||||
if !strings.ContainsRune(req.Email, '@') {
|
||||
respond(400, "Invalid Email Address", gc)
|
||||
return
|
||||
}
|
||||
id := gc.GetString("jfId")
|
||||
|
||||
// We'll use the ConfirmMyAction route to do the work, even if we don't need to confirm the address.
|
||||
claims := jwt.MapClaims{
|
||||
"valid": true,
|
||||
"id": id,
|
||||
"email": req.Email,
|
||||
"type": "confirmation",
|
||||
"target": UserEmailChange,
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
key, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to generate confirmation token: %v", err)
|
||||
respond(500, "errorUnknown", gc)
|
||||
return
|
||||
}
|
||||
|
||||
if emailEnabled && app.config.Section("email_confirmation").Key("enabled").MustBool(false) {
|
||||
user, status, err := app.jf.UserByID(id, false)
|
||||
name := ""
|
||||
if status == 200 && err == nil {
|
||||
name = user.Name
|
||||
}
|
||||
app.debug.Printf("%s: Email confirmation required", id)
|
||||
respond(401, "confirmEmail", gc)
|
||||
msg, err := app.email.constructConfirmation("", name, key, app, false)
|
||||
if err != nil {
|
||||
app.err.Printf("%s: Failed to construct confirmation email: %v", name, err)
|
||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
||||
app.err.Printf("%s: Failed to send user confirmation email: %v", name, err)
|
||||
} else {
|
||||
app.info.Printf("%s: Sent user confirmation email to \"%s\"", name, req.Email)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
app.confirmMyAction(gc, key)
|
||||
return
|
||||
}
|
||||
|
||||
// @Summary Returns a 10-minute, one-use Discord server invite
|
||||
// @Produce json
|
||||
// @Success 200 {object} DiscordInviteDTO
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Router /my/discord/invite [get]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MyDiscordServerInvite(gc *gin.Context) {
|
||||
if app.discord.inviteChannelName == "" {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
invURL, iconURL := app.discord.NewTempInvite(10*60, 1)
|
||||
if invURL == "" {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
gc.JSON(200, DiscordInviteDTO{invURL, iconURL})
|
||||
}
|
||||
|
||||
// @Summary Returns a linking PIN for discord/telegram
|
||||
// @Produce json
|
||||
// @Success 200 {object} GetMyPINDTO
|
||||
// @Failure 400 {object} stringResponse
|
||||
// Param service path string true "discord/telegram"
|
||||
// @Router /my/pin/{service} [get]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) GetMyPIN(gc *gin.Context) {
|
||||
service := gc.Param("service")
|
||||
resp := GetMyPINDTO{}
|
||||
switch service {
|
||||
case "discord":
|
||||
resp.PIN = app.discord.NewAssignedAuthToken(gc.GetString("jfId"))
|
||||
break
|
||||
case "telegram":
|
||||
resp.PIN = app.telegram.NewAssignedAuthToken(gc.GetString("jfId"))
|
||||
break
|
||||
default:
|
||||
respond(400, "invalid service", gc)
|
||||
return
|
||||
}
|
||||
gc.JSON(200, resp)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not your discord PIN was verified, and assigns the discord user to you.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Router /my/discord/verified/{pin} [get]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MyDiscordVerifiedInvite(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
dcUser, ok := app.discord.AssignedUserVerified(pin, gc.GetString("jfId"))
|
||||
app.discord.DeleteVerifiedUser(pin)
|
||||
if !ok {
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("discord").Key("require_unique").MustBool(false) && app.discord.UserExists(dcUser.ID) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
existingUser, ok := app.storage.GetDiscordKey(gc.GetString("jfId"))
|
||||
if ok {
|
||||
dcUser.Lang = existingUser.Lang
|
||||
dcUser.Contact = existingUser.Contact
|
||||
}
|
||||
app.storage.SetDiscordKey(gc.GetString("jfId"), dcUser)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Returns true/false on whether or not your telegram PIN was verified, and assigns the telegram user to you.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Router /my/telegram/verified/{pin} [get]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MyTelegramVerifiedInvite(gc *gin.Context) {
|
||||
pin := gc.Param("pin")
|
||||
token, ok := app.telegram.AssignedTokenVerified(pin, gc.GetString("jfId"))
|
||||
app.telegram.DeleteVerifiedToken(pin)
|
||||
if !ok {
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("telegram").Key("require_unique").MustBool(false) && app.telegram.UserExists(token.Username) {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
tgUser := TelegramUser{
|
||||
ChatID: token.ChatID,
|
||||
Username: token.Username,
|
||||
Contact: true,
|
||||
}
|
||||
if lang, ok := app.telegram.languages[tgUser.ChatID]; ok {
|
||||
tgUser.Lang = lang
|
||||
}
|
||||
|
||||
existingUser, ok := app.storage.GetTelegramKey(gc.GetString("jfId"))
|
||||
if ok {
|
||||
tgUser.Lang = existingUser.Lang
|
||||
tgUser.Contact = existingUser.Contact
|
||||
}
|
||||
app.storage.SetTelegramKey(gc.GetString("jfId"), tgUser)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Generate and send a new PIN to your given matrix user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 400 {object} stringResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Param MatrixSendPINDTO body MatrixSendPINDTO true "User's Matrix ID."
|
||||
// @Router /my/matrix/user [post]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MatrixSendMyPIN(gc *gin.Context) {
|
||||
var req MatrixSendPINDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.UserID == "" {
|
||||
respond(400, "errorNoUserID", gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("matrix").Key("require_unique").MustBool(false) {
|
||||
for _, u := range app.storage.GetMatrix() {
|
||||
if req.UserID == u.UserID {
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok := app.matrix.SendStart(req.UserID)
|
||||
if !ok {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Check whether your matrix PIN is valid, and link the account to yours if so.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Param pin path string true "PIN code to check"
|
||||
// @Param invCode path string true "invite Code"
|
||||
// @Param userID path string true "Matrix User ID"
|
||||
// @Router /my/matrix/verified/{userID}/{pin} [get]
|
||||
// @Security Bearer
|
||||
// @tags User Page
|
||||
func (app *appContext) MatrixCheckMyPIN(gc *gin.Context) {
|
||||
userID := gc.Param("userID")
|
||||
pin := gc.Param("pin")
|
||||
user, ok := app.matrix.tokens[pin]
|
||||
if !ok {
|
||||
app.debug.Println("Matrix: PIN not found")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
if user.User.UserID != userID {
|
||||
app.debug.Println("Matrix: User ID of PIN didn't match")
|
||||
respondBool(200, false, gc)
|
||||
return
|
||||
}
|
||||
|
||||
mxUser := *user.User
|
||||
mxUser.Contact = true
|
||||
existingUser, ok := app.storage.GetMatrixKey(gc.GetString("jfId"))
|
||||
if ok {
|
||||
mxUser.Lang = existingUser.Lang
|
||||
mxUser.Contact = existingUser.Contact
|
||||
}
|
||||
|
||||
app.storage.SetMatrixKey(gc.GetString("jfId"), mxUser)
|
||||
delete(app.matrix.tokens, pin)
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary unlink the Discord account from your Jellyfin user. Always succeeds.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Router /my/discord [delete]
|
||||
// @Security Bearer
|
||||
// @Tags User Page
|
||||
func (app *appContext) UnlinkMyDiscord(gc *gin.Context) {
|
||||
app.storage.DeleteDiscordKey(gc.GetString("jfId"))
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary unlink the Telegram account from your Jellyfin user. Always succeeds.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Router /my/telegram [delete]
|
||||
// @Security Bearer
|
||||
// @Tags User Page
|
||||
func (app *appContext) UnlinkMyTelegram(gc *gin.Context) {
|
||||
app.storage.DeleteTelegramKey(gc.GetString("jfId"))
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary unlink the Matrix account from your Jellyfin user. Always succeeds.
|
||||
// @Produce json
|
||||
// @Success 200 {object} boolResponse
|
||||
// @Router /my/matrix [delete]
|
||||
// @Security Bearer
|
||||
// @Tags User Page
|
||||
func (app *appContext) UnlinkMyMatrix(gc *gin.Context) {
|
||||
app.storage.DeleteMatrixKey(gc.GetString("jfId"))
|
||||
respondBool(200, true, gc)
|
||||
}
|
||||
|
||||
// @Summary Generate & send a password reset link if the given username/email/contact method exists. Doesn't give you any info about it's success.
|
||||
// @Produce json
|
||||
// @Param address path string true "address/contact method associated w/ your account."
|
||||
// @Success 204 {object} boolResponse
|
||||
// @Failure 400 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /my/password/reset/{address} [post]
|
||||
// @Tags User Page
|
||||
func (app *appContext) ResetMyPassword(gc *gin.Context) {
|
||||
// All requests should take 1 second, to make it harder to tell if a success occured or not.
|
||||
timerWait := make(chan bool)
|
||||
cancel := time.AfterFunc(1*time.Second, func() {
|
||||
timerWait <- true
|
||||
})
|
||||
address := gc.Param("address")
|
||||
if address == "" {
|
||||
app.debug.Println("Ignoring empty request for PWR")
|
||||
cancel.Stop()
|
||||
respondBool(400, false, gc)
|
||||
return
|
||||
}
|
||||
var pwr InternalPWR
|
||||
var err error
|
||||
|
||||
jfUser, ok := app.ReverseUserSearch(address)
|
||||
if !ok {
|
||||
app.debug.Printf("Ignoring PWR request: User not found")
|
||||
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
pwr, err = app.GenInternalReset(jfUser.ID)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to get user from Jellyfin: %v", err)
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if app.internalPWRs == nil {
|
||||
app.internalPWRs = map[string]InternalPWR{}
|
||||
}
|
||||
app.internalPWRs[pwr.PIN] = pwr
|
||||
// FIXME: Send to all contact methods
|
||||
msg, err := app.email.constructReset(
|
||||
PasswordReset{
|
||||
Pin: pwr.PIN,
|
||||
Username: pwr.Username,
|
||||
Expiry: pwr.Expiry,
|
||||
Internal: true,
|
||||
}, app, false,
|
||||
)
|
||||
if err != nil {
|
||||
app.err.Printf("Failed to construct password reset message for \"%s\": %v", pwr.Username, err)
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
}
|
||||
return
|
||||
} else if err := app.sendByID(msg, jfUser.ID); err != nil {
|
||||
app.err.Printf("Failed to send password reset message to \"%s\": %v", address, err)
|
||||
} else {
|
||||
app.info.Printf("Sent password reset message to \"%s\"", address)
|
||||
}
|
||||
for range timerWait {
|
||||
respondBool(204, true, gc)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Change your password, given the old one and the new one.
|
||||
// @Produce json
|
||||
// @Param ChangeMyPasswordDTO body ChangeMyPasswordDTO true "User's old & new passwords."
|
||||
// @Success 204 {object} boolResponse
|
||||
// @Failure 400 {object} PasswordValidation
|
||||
// @Failure 401 {object} boolResponse
|
||||
// @Failure 500 {object} boolResponse
|
||||
// @Router /my/password [post]
|
||||
// @Security Bearer
|
||||
// @Tags User Page
|
||||
func (app *appContext) ChangeMyPassword(gc *gin.Context) {
|
||||
var req ChangeMyPasswordDTO
|
||||
gc.BindJSON(&req)
|
||||
if req.Old == "" || req.New == "" {
|
||||
respondBool(400, false, gc)
|
||||
}
|
||||
validation := app.validator.validate(req.New)
|
||||
for _, val := range validation {
|
||||
if !val {
|
||||
app.debug.Printf("%s: Change password failed: Invalid password", gc.GetString("jfId"))
|
||||
gc.JSON(400, validation)
|
||||
return
|
||||
}
|
||||
}
|
||||
user, status, err := app.jf.UserByID(gc.GetString("jfId"), false)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to change password: couldn't find user (%d): %+v", status, err)
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
// Authenticate as user to confirm old password.
|
||||
user, status, err = app.authJf.Authenticate(user.Name, req.Old)
|
||||
if status != 200 || err != nil {
|
||||
respondBool(401, false, gc)
|
||||
return
|
||||
}
|
||||
status, err = app.jf.SetPassword(gc.GetString("jfId"), req.Old, req.New)
|
||||
if (status != 200 && status != 204) || err != nil {
|
||||
respondBool(500, false, gc)
|
||||
return
|
||||
}
|
||||
if app.config.Section("ombi").Key("enabled").MustBool(false) {
|
||||
func() {
|
||||
ombiUser, status, err := app.getOmbiUser(gc.GetString("jfId"))
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to get user \"%s\" from ombi (%d): %v", user.Name, status, err)
|
||||
return
|
||||
}
|
||||
ombiUser["password"] = req.New
|
||||
status, err = app.ombi.ModifyUser(ombiUser)
|
||||
if status != 200 || err != nil {
|
||||
app.err.Printf("Failed to set password for ombi user \"%s\" (%d): %v", ombiUser["userName"], status, err)
|
||||
return
|
||||
}
|
||||
app.debug.Printf("Reset password for ombi user \"%s\"", ombiUser["userName"])
|
||||
}()
|
||||
}
|
||||
cookie, err := gc.Cookie("user-refresh")
|
||||
if err == nil {
|
||||
app.invalidTokens = append(app.invalidTokens, cookie)
|
||||
gc.SetCookie("refresh", "invalid", -1, "/my", gc.Request.URL.Hostname(), true, true)
|
||||
} else {
|
||||
app.debug.Printf("Couldn't get cookies: %s", err)
|
||||
}
|
||||
respondBool(204, true, gc)
|
||||
}
|
||||
1131
api-users.go
Normal file
159
args.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (app *appContext) loadArgs(firstCall bool) {
|
||||
if firstCall {
|
||||
flag.Usage = helpFunc
|
||||
help := flag.Bool("help", false, "prints this message.")
|
||||
flag.BoolVar(help, "h", false, "SHORTHAND")
|
||||
|
||||
DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
|
||||
flag.StringVar(DATA, "d", app.dataPath, "SHORTHAND")
|
||||
CONFIG = flag.String("config", app.configPath, "alternate path to config file.")
|
||||
flag.StringVar(CONFIG, "c", app.configPath, "SHORTHAND")
|
||||
HOST = flag.String("host", "", "alternate address to host web ui on.")
|
||||
PORT = flag.Int("port", 0, "alternate port to host web ui on.")
|
||||
flag.IntVar(PORT, "p", 0, "SHORTHAND")
|
||||
DEBUG = flag.Bool("debug", false, "Enables debug logging.")
|
||||
PPROF = flag.Bool("pprof", false, "Exposes pprof profiler on /debug/pprof.")
|
||||
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html")
|
||||
|
||||
flag.Parse()
|
||||
if *help {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
if *SWAGGER {
|
||||
os.Setenv("SWAGGER", "1")
|
||||
}
|
||||
if *DEBUG {
|
||||
os.Setenv("DEBUG", "1")
|
||||
}
|
||||
if *PPROF {
|
||||
os.Setenv("PPROF", "1")
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("SWAGGER") == "1" {
|
||||
*SWAGGER = true
|
||||
}
|
||||
if os.Getenv("DEBUG") == "1" {
|
||||
*DEBUG = true
|
||||
}
|
||||
if os.Getenv("PPROF") == "1" {
|
||||
*PPROF = true
|
||||
}
|
||||
// attempt to apply command line flags correctly
|
||||
if app.configPath == *CONFIG && app.dataPath != *DATA {
|
||||
app.dataPath = *DATA
|
||||
app.configPath = filepath.Join(app.dataPath, "config.ini")
|
||||
} else if app.configPath != *CONFIG && app.dataPath == *DATA {
|
||||
app.configPath = *CONFIG
|
||||
} else {
|
||||
app.configPath = *CONFIG
|
||||
app.dataPath = *DATA
|
||||
}
|
||||
|
||||
// Previously used for self-restarts but leaving them here as they might be useful.
|
||||
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
|
||||
app.configPath = v
|
||||
}
|
||||
if v := os.Getenv("JFA_DATAPATH"); v != "" {
|
||||
app.dataPath = v
|
||||
}
|
||||
|
||||
os.Setenv("JFA_CONFIGPATH", app.configPath)
|
||||
os.Setenv("JFA_DATAPATH", app.dataPath)
|
||||
}
|
||||
|
||||
/*
|
||||
Adds start/stop/systemd to help message, and
|
||||
|
||||
also gets rid of usage for shorthand flags, and merge them with the full-length one.
|
||||
implementation is 🤢, will clean this up eventually.
|
||||
|
||||
-h SHORTHAND
|
||||
-help
|
||||
prints this message.
|
||||
|
||||
becomes:
|
||||
|
||||
-help, -h
|
||||
prints this message.
|
||||
*/
|
||||
func helpFunc() {
|
||||
fmt.Fprint(stderr, `Usage of jfa-go:
|
||||
start
|
||||
start jfa-go as a daemon and run in the background.
|
||||
stop
|
||||
stop a daemonized instance of jfa-go.
|
||||
systemd
|
||||
generate a systemd .service file.
|
||||
`)
|
||||
shortHands := []string{"-help", "-data", "-config", "-port"}
|
||||
var b bytes.Buffer
|
||||
// Write defaults into buffer then remove any shorthands
|
||||
flag.CommandLine.SetOutput(&b)
|
||||
flag.PrintDefaults()
|
||||
flag.CommandLine.SetOutput(stderr)
|
||||
scanner := bufio.NewScanner(&b)
|
||||
out := ""
|
||||
line := scanner.Text()
|
||||
eof := !scanner.Scan()
|
||||
lastLine := false
|
||||
for !eof || lastLine {
|
||||
nextline := scanner.Text()
|
||||
start := 0
|
||||
if len(nextline) != 0 {
|
||||
for nextline[start] == ' ' && start < len(nextline) {
|
||||
start++
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "SHORTHAND") || (len(nextline) != 0 && strings.Contains(nextline, "SHORTHAND") && nextline[start] != '-') {
|
||||
line = nextline
|
||||
if lastLine {
|
||||
break
|
||||
}
|
||||
eof := !scanner.Scan()
|
||||
if eof {
|
||||
lastLine = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// if !strings.Contains(line, "SHORTHAND") && !(strings.Contains(nextline, "SHORTHAND") && !strings.Contains(nextline, "-")) {
|
||||
match := false
|
||||
for i, c := range line {
|
||||
if c != '-' {
|
||||
continue
|
||||
}
|
||||
for _, s := range shortHands {
|
||||
if i+len(s) <= len(line) && line[i:i+len(s)] == s {
|
||||
out += line[:i+len(s)] + ", " + s[:2] + line[i+len(s):] + "\n"
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
out += line + "\n"
|
||||
}
|
||||
line = nextline
|
||||
if lastLine {
|
||||
break
|
||||
}
|
||||
eof := !scanner.Scan()
|
||||
if eof {
|
||||
lastLine = true
|
||||
}
|
||||
}
|
||||
fmt.Fprint(stderr, out)
|
||||
}
|
||||
182
auth.go
@@ -4,36 +4,41 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/hrfee/mediabrowser"
|
||||
"github.com/lithammer/shortuuid/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
TOKEN_VALIDITY_SEC = 20 * 60
|
||||
REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24
|
||||
)
|
||||
|
||||
func (app *appContext) webAuth() gin.HandlerFunc {
|
||||
return app.authenticate
|
||||
}
|
||||
|
||||
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||
func CreateToken(userId, jfId string) (string, string, error) {
|
||||
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
||||
var token, refresh string
|
||||
claims := jwt.MapClaims{
|
||||
"valid": true,
|
||||
"id": userId,
|
||||
"exp": strconv.FormatInt(time.Now().Add(time.Minute*20).Unix(), 10),
|
||||
"exp": time.Now().Add(time.Second * TOKEN_VALIDITY_SEC).Unix(),
|
||||
"jfid": jfId,
|
||||
"admin": admin,
|
||||
"type": "bearer",
|
||||
}
|
||||
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
claims["exp"] = strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10)
|
||||
claims["exp"] = time.Now().Add(time.Second * REFRESH_TOKEN_VALIDITY_SEC).Unix()
|
||||
claims["type"] = "refresh"
|
||||
tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||
@@ -43,8 +48,9 @@ func CreateToken(userId, jfId string) (string, string, error) {
|
||||
return token, refresh, nil
|
||||
}
|
||||
|
||||
// Check header for token
|
||||
func (app *appContext) authenticate(gc *gin.Context) {
|
||||
// Caller should return if this returns false.
|
||||
func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.MapClaims, ok bool) {
|
||||
ok = false
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
if header[0] != "Bearer" {
|
||||
app.debug.Println("Invalid authorization header")
|
||||
@@ -57,23 +63,48 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
app.debug.Println("Invalid JWT")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
expiryUnix := int64(claims["exp"].(float64))
|
||||
if err != nil {
|
||||
app.debug.Printf("Auth denied: %s", err)
|
||||
respond(401, "Unauthorized", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("Auth denied: Invalid token")
|
||||
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
||||
respond(401, "Unauthorized", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// Check header for token
|
||||
func (app *appContext) authenticate(gc *gin.Context) {
|
||||
claims, ok := app.decodeValidateAuthHeader(gc)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
isAdminToken := claims["admin"].(bool)
|
||||
if !isAdminToken {
|
||||
app.debug.Printf("Auth denied: Token was not for admin access")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
|
||||
userID := claims["id"].(string)
|
||||
jfID := claims["jfid"].(string)
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
for _, user := range app.adminUsers {
|
||||
if user.UserID == userID {
|
||||
match = true
|
||||
break
|
||||
@@ -86,6 +117,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
||||
}
|
||||
gc.Set("jfId", jfID)
|
||||
gc.Set("userId", userID)
|
||||
gc.Set("userMode", false)
|
||||
app.debug.Println("Auth succeeded")
|
||||
gc.Next()
|
||||
}
|
||||
@@ -101,8 +133,46 @@ type getTokenDTO struct {
|
||||
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else.
|
||||
}
|
||||
|
||||
func (app *appContext) decodeValidateLoginHeader(gc *gin.Context) (username, password string, ok bool) {
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
username = creds[0]
|
||||
password = creds[1]
|
||||
ok = false
|
||||
if username == "" || password == "" {
|
||||
app.debug.Println("Auth denied: blank username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (app *appContext) validateJellyfinCredentials(username, password string, gc *gin.Context) (user mediabrowser.User, ok bool) {
|
||||
ok = false
|
||||
user, status, err := app.authJf.Authenticate(username, password)
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 || status == 400 {
|
||||
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
if status == 403 {
|
||||
app.info.Println("Auth denied: Jellyfin account disabled")
|
||||
respond(403, "yourAccountWasDisabled", gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||
respond(500, "Jellyfin error", gc)
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// @Summary Grabs an API token using username & password.
|
||||
// @description Click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and set the API key as "Bearer `your api key`".
|
||||
// @description If viewing docs locally, click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and set the API key as "Bearer `your api key`".
|
||||
// @Produce json
|
||||
// @Success 200 {object} getTokenDTO
|
||||
// @Failure 401 {object} stringResponse
|
||||
@@ -111,18 +181,14 @@ type getTokenDTO struct {
|
||||
// @Security getTokenAuth
|
||||
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
app.info.Println("Token requested (login attempt)")
|
||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
||||
auth, _ := base64.StdEncoding.DecodeString(header[1])
|
||||
creds := strings.SplitN(string(auth), ":", 2)
|
||||
var userID, jfID string
|
||||
if creds[0] == "" || creds[1] == "" {
|
||||
app.debug.Println("Auth denied: blank username/password")
|
||||
respond(401, "Unauthorized", gc)
|
||||
username, password, ok := app.decodeValidateLoginHeader(gc)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var userID, jfID string
|
||||
match := false
|
||||
for _, user := range app.users {
|
||||
if user.Username == creds[0] && user.Password == creds[1] {
|
||||
for _, user := range app.adminUsers {
|
||||
if user.Username == username && user.Password == password {
|
||||
match = true
|
||||
app.debug.Println("Found existing user")
|
||||
userID = user.UserID
|
||||
@@ -135,24 +201,20 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
var status int
|
||||
var err error
|
||||
var user map[string]interface{}
|
||||
user, status, err = app.authJf.Authenticate(creds[0], creds[1])
|
||||
if status != 200 || err != nil {
|
||||
if status == 401 || status == 400 {
|
||||
app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
||||
respond(500, "Jellyfin error", gc)
|
||||
user, ok := app.validateJellyfinCredentials(username, password, gc)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jfID = user["Id"].(string)
|
||||
if app.config.Section("ui").Key("admin_only").MustBool(true) {
|
||||
if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0])
|
||||
jfID = user.ID
|
||||
if !app.config.Section("ui").Key("allow_all").MustBool(false) {
|
||||
accountsAdmin := false
|
||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||
if emailStore, ok := app.storage.GetEmailsKey(jfID); ok {
|
||||
accountsAdmin = emailStore.Admin
|
||||
}
|
||||
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
||||
if !accountsAdmin {
|
||||
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
||||
respond(401, "Unauthorized", gc)
|
||||
return
|
||||
}
|
||||
@@ -162,10 +224,10 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
newUser := User{
|
||||
UserID: userID,
|
||||
}
|
||||
app.debug.Printf("Token generated for user \"%s\"", creds[0])
|
||||
app.users = append(app.users, newUser)
|
||||
app.debug.Printf("Token generated for user \"%s\"", username)
|
||||
app.adminUsers = append(app.adminUsers, newUser)
|
||||
}
|
||||
token, refresh, err := CreateToken(userID, jfID)
|
||||
token, refresh, err := CreateToken(userID, jfID, true)
|
||||
if err != nil {
|
||||
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
@@ -175,15 +237,9 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
||||
gc.JSON(200, getTokenDTO{token})
|
||||
}
|
||||
|
||||
// @Summary Grabs an API token using a refresh token from cookies.
|
||||
// @Produce json
|
||||
// @Success 200 {object} getTokenDTO
|
||||
// @Failure 401 {object} stringResponse
|
||||
// @Router /token/refresh [get]
|
||||
// @tags Auth
|
||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
app.debug.Println("Token requested (refresh token)")
|
||||
cookie, err := gc.Cookie("refresh")
|
||||
func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName string) (claims jwt.MapClaims, ok bool) {
|
||||
ok = false
|
||||
cookie, err := gc.Cookie(cookieName)
|
||||
if err != nil || cookie == "" {
|
||||
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
||||
respond(400, "Couldn't get token", gc)
|
||||
@@ -202,27 +258,45 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
respond(400, "Invalid token", gc)
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
|
||||
claims, ok = token.Claims.(jwt.MapClaims)
|
||||
expiryUnix := int64(claims["exp"].(float64))
|
||||
if err != nil {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
expiry := time.Unix(expiryUnix, 0)
|
||||
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %s", err)
|
||||
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
||||
respond(401, "Invalid token", gc)
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
// @Summary Grabs an API token using a refresh token from cookies.
|
||||
// @Produce json
|
||||
// @Success 200 {object} getTokenDTO
|
||||
// @Failure 401 {object} stringResponse
|
||||
// @Router /token/refresh [get]
|
||||
// @tags Auth
|
||||
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
||||
app.debug.Println("Token requested (refresh token)")
|
||||
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
userID := claims["id"].(string)
|
||||
jfID := claims["jfid"].(string)
|
||||
jwt, refresh, err := CreateToken(userID, jfID)
|
||||
jwt, refresh, err := CreateToken(userID, jfID, true)
|
||||
if err != nil {
|
||||
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
||||
respond(500, "Couldn't generate token", gc)
|
||||
return
|
||||
}
|
||||
gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.SetCookie("refresh", refresh, REFRESH_TOKEN_VALIDITY_SEC, "/", gc.Request.URL.Hostname(), true, true)
|
||||
gc.JSON(200, getTokenDTO{jwt})
|
||||
}
|
||||
|
||||
69
autostart.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// +build tray
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/emersion/go-autostart"
|
||||
"github.com/getlantern/systray"
|
||||
)
|
||||
|
||||
type Autostart struct {
|
||||
as *autostart.App
|
||||
enabled bool
|
||||
menuitem *systray.MenuItem
|
||||
clicked chan bool
|
||||
}
|
||||
|
||||
func NewAutostart(name, displayname, trayName, trayTooltip string) *Autostart {
|
||||
a := &Autostart{
|
||||
as: &autostart.App{
|
||||
Name: name,
|
||||
DisplayName: displayname,
|
||||
},
|
||||
enabled: true,
|
||||
clicked: make(chan bool),
|
||||
}
|
||||
a.menuitem = systray.AddMenuItemCheckbox(trayName, trayTooltip, a.as.IsEnabled())
|
||||
command := os.Args
|
||||
command[0], _ = filepath.Abs(command[0])
|
||||
// Make sure to replace any relative paths with absolute ones
|
||||
pathArgs := []string{"-d", "-data", "-c", "-config"}
|
||||
for i := 1; i < len(command); i++ {
|
||||
isPath := false
|
||||
for _, p := range pathArgs {
|
||||
if command[i-1] == p {
|
||||
isPath = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isPath {
|
||||
command[i], _ = filepath.Abs(command[i])
|
||||
}
|
||||
}
|
||||
a.as.Exec = command
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Autostart) HandleCheck() {
|
||||
for range a.menuitem.ClickedCh {
|
||||
if !a.menuitem.Checked() {
|
||||
if err := a.as.Enable(); err != nil {
|
||||
log.Printf("Failed to enable autostart on login: %v", err)
|
||||
} else {
|
||||
a.menuitem.Check()
|
||||
log.Printf("Enabled autostart")
|
||||
}
|
||||
} else {
|
||||
if err := a.as.Disable(); err != nil {
|
||||
log.Printf("Failed to disable autostart on login: %v", err)
|
||||
} else {
|
||||
a.menuitem.Uncheck()
|
||||
log.Printf("Disabled autostart")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
"log"
|
||||
)
|
||||
|
||||
// TimeoutHandler recovers from an http timeout.
|
||||
// TimeoutHandler recovers from an http timeout or panic.
|
||||
type TimeoutHandler func()
|
||||
|
||||
// NewTimeoutHandler returns a new Timeout handler.
|
||||
func NewTimeoutHandler(name, addr string, noFail bool) TimeoutHandler {
|
||||
return func() {
|
||||
if r := recover(); r != nil {
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ %s: Timed out", name, addr)
|
||||
out := fmt.Sprintf("Failed to authenticate with %s @ \"%s\": Timed out", name, addr)
|
||||
if noFail {
|
||||
log.Print(out)
|
||||
} else {
|
||||
|
||||
183
config.go
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -9,31 +11,24 @@ import (
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
/*var DeCamel ini.NameMapper = func(raw string) string {
|
||||
out := make([]rune, 0, len(raw))
|
||||
upper := 0
|
||||
for _, c := range raw {
|
||||
if unicode.IsUpper(c) {
|
||||
upper++
|
||||
}
|
||||
if upper == 2 {
|
||||
out = append(out, '_')
|
||||
upper = 0
|
||||
}
|
||||
out = append(out, unicode.ToLower(c))
|
||||
var emailEnabled = false
|
||||
var messagesEnabled = false
|
||||
var telegramEnabled = false
|
||||
var discordEnabled = false
|
||||
var matrixEnabled = false
|
||||
|
||||
func (app *appContext) GetPath(sect, key string) (fs.FS, string) {
|
||||
val := app.config.Section(sect).Key(key).MustString("")
|
||||
if strings.HasPrefix(val, "jfa-go:") {
|
||||
return localFS, strings.TrimPrefix(val, "jfa-go:")
|
||||
}
|
||||
return string(out)
|
||||
dir, file := filepath.Split(val)
|
||||
return os.DirFS(dir), file
|
||||
}
|
||||
|
||||
func (app *appContext) loadDefaults() (err error) {
|
||||
var cfb []byte
|
||||
cfb, err = ioutil.ReadFile(app.configBase_path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(cfb, app.defaults)
|
||||
return
|
||||
}*/
|
||||
func (app *appContext) MustSetValue(section, key, val string) {
|
||||
app.config.Section(section).Key(key).SetValue(app.config.Section(section).Key(key).MustString(val))
|
||||
}
|
||||
|
||||
func (app *appContext) loadConfig() error {
|
||||
var err error
|
||||
@@ -42,43 +37,145 @@ func (app *appContext) loadConfig() error {
|
||||
return err
|
||||
}
|
||||
|
||||
app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String()))
|
||||
app.MustSetValue("jellyfin", "public_server", app.config.Section("jellyfin").Key("server").String())
|
||||
|
||||
app.MustSetValue("ui", "redirect_url", app.config.Section("jellyfin").Key("public_server").String())
|
||||
|
||||
for _, key := range app.config.Section("files").Keys() {
|
||||
// if key.MustString("") == "" && key.Name() != "custom_css" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
if key.Name() != "html_templates" {
|
||||
if name := key.Name(); name != "html_templates" && name != "lang_files" {
|
||||
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template"} {
|
||||
// if app.config.Section("files").Key(key).MustString("") == "" {
|
||||
// key.SetValue(filepath.Join(app.data_path, (key.Name() + ".json")))
|
||||
// }
|
||||
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails", "users", "telegram_users", "discord_users", "matrix_users", "announcements", "custom_user_page_content"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
|
||||
}
|
||||
for _, key := range []string{"matrix_sql"} {
|
||||
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".db"))))
|
||||
}
|
||||
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
|
||||
app.config.Section("email").Key("no_username").SetValue(strconv.FormatBool(app.config.Section("email").Key("no_username").MustBool(false)))
|
||||
|
||||
app.config.Section("password_resets").Key("email_html").SetValue(app.config.Section("password_resets").Key("email_html").MustString(filepath.Join(app.localPath, "email.html")))
|
||||
app.config.Section("password_resets").Key("email_text").SetValue(app.config.Section("password_resets").Key("email_text").MustString(filepath.Join(app.localPath, "email.txt")))
|
||||
app.MustSetValue("password_resets", "email_html", "jfa-go:"+"email.html")
|
||||
app.MustSetValue("password_resets", "email_text", "jfa-go:"+"email.txt")
|
||||
|
||||
app.config.Section("invite_emails").Key("email_html").SetValue(app.config.Section("invite_emails").Key("email_html").MustString(filepath.Join(app.localPath, "invite-email.html")))
|
||||
app.config.Section("invite_emails").Key("email_text").SetValue(app.config.Section("invite_emails").Key("email_text").MustString(filepath.Join(app.localPath, "invite-email.txt")))
|
||||
app.MustSetValue("invite_emails", "email_html", "jfa-go:"+"invite-email.html")
|
||||
app.MustSetValue("invite_emails", "email_text", "jfa-go:"+"invite-email.txt")
|
||||
|
||||
app.config.Section("notifications").Key("expiry_html").SetValue(app.config.Section("notifications").Key("expiry_html").MustString(filepath.Join(app.localPath, "expired.html")))
|
||||
app.config.Section("notifications").Key("expiry_text").SetValue(app.config.Section("notifications").Key("expiry_text").MustString(filepath.Join(app.localPath, "expired.txt")))
|
||||
app.MustSetValue("email_confirmation", "email_html", "jfa-go:"+"confirmation.html")
|
||||
app.MustSetValue("email_confirmation", "email_text", "jfa-go:"+"confirmation.txt")
|
||||
|
||||
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.localPath, "created.html")))
|
||||
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.localPath, "created.txt")))
|
||||
app.MustSetValue("notifications", "expiry_html", "jfa-go:"+"expired.html")
|
||||
app.MustSetValue("notifications", "expiry_text", "jfa-go:"+"expired.txt")
|
||||
|
||||
app.config.Section("deletion").Key("email_html").SetValue(app.config.Section("deletion").Key("email_html").MustString(filepath.Join(app.localPath, "deleted.html")))
|
||||
app.config.Section("deletion").Key("email_text").SetValue(app.config.Section("deletion").Key("email_text").MustString(filepath.Join(app.localPath, "deleted.txt")))
|
||||
app.MustSetValue("notifications", "created_html", "jfa-go:"+"created.html")
|
||||
app.MustSetValue("notifications", "created_text", "jfa-go:"+"created.txt")
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(VERSION)
|
||||
app.MustSetValue("deletion", "email_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("deletion", "email_text", "jfa-go:"+"deleted.txt")
|
||||
|
||||
app.MustSetValue("smtp", "hello_hostname", "localhost")
|
||||
app.MustSetValue("smtp", "cert_validation", "true")
|
||||
|
||||
sc := app.config.Section("discord").Key("start_command").MustString("start")
|
||||
app.config.Section("discord").Key("start_command").SetValue(strings.TrimPrefix(strings.TrimPrefix(sc, "/"), "!"))
|
||||
|
||||
jfUrl := app.config.Section("jellyfin").Key("server").String()
|
||||
if !(strings.HasPrefix(jfUrl, "http://") || strings.HasPrefix(jfUrl, "https://")) {
|
||||
app.config.Section("jellyfin").Key("server").SetValue("http://" + jfUrl)
|
||||
}
|
||||
|
||||
// Deletion template is good enough for these as well.
|
||||
app.MustSetValue("disable_enable", "disabled_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("disable_enable", "disabled_text", "jfa-go:"+"deleted.txt")
|
||||
app.MustSetValue("disable_enable", "enabled_html", "jfa-go:"+"deleted.html")
|
||||
app.MustSetValue("disable_enable", "enabled_text", "jfa-go:"+"deleted.txt")
|
||||
|
||||
app.MustSetValue("welcome_email", "email_html", "jfa-go:"+"welcome.html")
|
||||
app.MustSetValue("welcome_email", "email_text", "jfa-go:"+"welcome.txt")
|
||||
|
||||
app.MustSetValue("template_email", "email_html", "jfa-go:"+"template.html")
|
||||
app.MustSetValue("template_email", "email_text", "jfa-go:"+"template.txt")
|
||||
|
||||
app.MustSetValue("user_expiry", "behaviour", "disable_user")
|
||||
app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html")
|
||||
app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt")
|
||||
|
||||
app.MustSetValue("matrix", "topic", "Jellyfin notifications")
|
||||
app.MustSetValue("matrix", "show_on_reg", "true")
|
||||
|
||||
app.MustSetValue("discord", "show_on_reg", "true")
|
||||
|
||||
app.MustSetValue("telegram", "show_on_reg", "true")
|
||||
|
||||
app.config.Section("jellyfin").Key("version").SetValue(version)
|
||||
app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", VERSION, COMMIT))
|
||||
app.config.Section("jellyfin").Key("device_id").SetValue(fmt.Sprintf("jfa-go-%s-%s", version, commit))
|
||||
|
||||
// These two settings are pretty much the same
|
||||
url1 := app.config.Section("invite_emails").Key("url_base").String()
|
||||
url2 := app.config.Section("password_resets").Key("url_base").String()
|
||||
app.MustSetValue("password_resets", "url_base", strings.TrimSuffix(url1, "/invite"))
|
||||
app.MustSetValue("invite_emails", "url_base", url2)
|
||||
|
||||
messagesEnabled = app.config.Section("messages").Key("enabled").MustBool(false)
|
||||
telegramEnabled = app.config.Section("telegram").Key("enabled").MustBool(false)
|
||||
discordEnabled = app.config.Section("discord").Key("enabled").MustBool(false)
|
||||
matrixEnabled = app.config.Section("matrix").Key("enabled").MustBool(false)
|
||||
if !messagesEnabled {
|
||||
emailEnabled = false
|
||||
telegramEnabled = false
|
||||
discordEnabled = false
|
||||
matrixEnabled = false
|
||||
} else if app.config.Section("email").Key("method").MustString("") == "" {
|
||||
emailEnabled = false
|
||||
} else {
|
||||
emailEnabled = true
|
||||
}
|
||||
if !emailEnabled && !telegramEnabled && !discordEnabled && !matrixEnabled {
|
||||
messagesEnabled = false
|
||||
}
|
||||
|
||||
app.MustSetValue("updates", "enabled", "true")
|
||||
releaseChannel := app.config.Section("updates").Key("channel").String()
|
||||
if app.config.Section("updates").Key("enabled").MustBool(false) {
|
||||
v := version
|
||||
if releaseChannel == "stable" {
|
||||
if version == "git" {
|
||||
v = "0.0.0"
|
||||
}
|
||||
} else if releaseChannel == "unstable" {
|
||||
v = "git"
|
||||
}
|
||||
app.updater = newUpdater(baseURL, namespace, repo, v, commit, updater)
|
||||
}
|
||||
if releaseChannel == "" {
|
||||
if version == "git" {
|
||||
releaseChannel = "unstable"
|
||||
} else {
|
||||
releaseChannel = "stable"
|
||||
}
|
||||
app.MustSetValue("updates", "channel", releaseChannel)
|
||||
}
|
||||
|
||||
substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
|
||||
|
||||
if substituteStrings != "" {
|
||||
v := app.config.Section("ui").Key("success_message")
|
||||
v.SetValue(strings.ReplaceAll(v.String(), "Jellyfin", substituteStrings))
|
||||
}
|
||||
|
||||
oldFormLang := app.config.Section("ui").Key("language").MustString("")
|
||||
if oldFormLang != "" {
|
||||
app.storage.lang.chosenUserLang = oldFormLang
|
||||
}
|
||||
newFormLang := app.config.Section("ui").Key("language-form").MustString("")
|
||||
if newFormLang != "" {
|
||||
app.storage.lang.chosenUserLang = newFormLang
|
||||
}
|
||||
app.storage.lang.chosenAdminLang = app.config.Section("ui").Key("language-admin").MustString("en-us")
|
||||
app.storage.lang.chosenEmailLang = app.config.Section("email").Key("language").MustString("en-us")
|
||||
app.storage.lang.chosenPWRLang = app.config.Section("password_resets").Key("language").MustString("en-us")
|
||||
app.storage.lang.chosenTelegramLang = app.config.Section("telegram").Key("language").MustString("en-us")
|
||||
|
||||
app.email = NewEmailer(app)
|
||||
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
### fixconfig
|
||||
|
||||
Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so this script opens the json file, and for each section, adds an "order" list which tells the web page in which order to display settings.
|
||||
|
||||
Python's `json` library retains the order of data in a JSON file, which meant settings sent to the web page would be in the right order. Go's `encoding/json` and maps do not retain order, so `enumerate/enumerate_config.py` opens the json file, and for each section, adds an "order" array which tells the web page in which order to display settings.
|
||||
Specify the input and output files with `-i` and `-o` respectively.
|
||||
|
||||
### jsontostruct
|
||||
|
||||
Generates a go struct from `config-base.json`. I wrote this because i was annoyed with the `ini` library, but i've since realised mapping the ini values onto it is painful.
|
||||
|
||||
|
||||
|
||||
@@ -1,541 +0,0 @@
|
||||
package main
|
||||
|
||||
type Metadata struct{
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type Config struct{
|
||||
Order []string `json:"order"`
|
||||
Jellyfin struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Username struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"username"`
|
||||
} `json:"username" cfg:"username"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
Server struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"server"`
|
||||
} `json:"server" cfg:"server"`
|
||||
PublicServer struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"public_server"`
|
||||
} `json:"public_server" cfg:"public_server"`
|
||||
Client struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"client"`
|
||||
} `json:"client" cfg:"client"`
|
||||
Version struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"version"`
|
||||
} `json:"version" cfg:"version"`
|
||||
Device struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"device"`
|
||||
} `json:"device" cfg:"device"`
|
||||
DeviceId struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"device_id"`
|
||||
} `json:"device_id" cfg:"device_id"`
|
||||
} `json:"jellyfin"`
|
||||
Ui struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Theme struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"theme"`
|
||||
} `json:"theme" cfg:"theme"`
|
||||
Host struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"host"`
|
||||
} `json:"host" cfg:"host"`
|
||||
Port struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value" cfg:"port"`
|
||||
} `json:"port" cfg:"port"`
|
||||
JellyfinLogin struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"jellyfin_login"`
|
||||
} `json:"jellyfin_login" cfg:"jellyfin_login"`
|
||||
AdminOnly struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"admin_only"`
|
||||
} `json:"admin_only" cfg:"admin_only"`
|
||||
Username struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"username"`
|
||||
} `json:"username" cfg:"username"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
Email struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email"`
|
||||
} `json:"email" cfg:"email"`
|
||||
Debug struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"debug"`
|
||||
} `json:"debug" cfg:"debug"`
|
||||
ContactMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"contact_message"`
|
||||
} `json:"contact_message" cfg:"contact_message"`
|
||||
HelpMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"help_message"`
|
||||
} `json:"help_message" cfg:"help_message"`
|
||||
SuccessMessage struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"success_message"`
|
||||
} `json:"success_message" cfg:"success_message"`
|
||||
Bs5 struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"bs5"`
|
||||
} `json:"bs5" cfg:"bs5"`
|
||||
} `json:"ui"`
|
||||
PasswordValidation struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
MinLength struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"min_length"`
|
||||
} `json:"min_length" cfg:"min_length"`
|
||||
Upper struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"upper"`
|
||||
} `json:"upper" cfg:"upper"`
|
||||
Lower struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"lower"`
|
||||
} `json:"lower" cfg:"lower"`
|
||||
Number struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"number"`
|
||||
} `json:"number" cfg:"number"`
|
||||
Special struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"special"`
|
||||
} `json:"special" cfg:"special"`
|
||||
} `json:"password_validation"`
|
||||
Email struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
NoUsername struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"no_username"`
|
||||
} `json:"no_username" cfg:"no_username"`
|
||||
Use24H struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"use_24h"`
|
||||
} `json:"use_24h" cfg:"use_24h"`
|
||||
DateFormat struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"date_format"`
|
||||
} `json:"date_format" cfg:"date_format"`
|
||||
Message struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"message"`
|
||||
} `json:"message" cfg:"message"`
|
||||
Method struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"method"`
|
||||
} `json:"method" cfg:"method"`
|
||||
Address struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"address"`
|
||||
} `json:"address" cfg:"address"`
|
||||
From struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"from"`
|
||||
} `json:"from" cfg:"from"`
|
||||
} `json:"email"`
|
||||
PasswordResets struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
WatchDirectory struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"watch_directory"`
|
||||
} `json:"watch_directory" cfg:"watch_directory"`
|
||||
EmailHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_html"`
|
||||
} `json:"email_html" cfg:"email_html"`
|
||||
EmailText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_text"`
|
||||
} `json:"email_text" cfg:"email_text"`
|
||||
Subject struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"subject"`
|
||||
} `json:"subject" cfg:"subject"`
|
||||
} `json:"password_resets"`
|
||||
InviteEmails struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
EmailHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_html"`
|
||||
} `json:"email_html" cfg:"email_html"`
|
||||
EmailText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"email_text"`
|
||||
} `json:"email_text" cfg:"email_text"`
|
||||
Subject struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"subject"`
|
||||
} `json:"subject" cfg:"subject"`
|
||||
UrlBase struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"url_base"`
|
||||
} `json:"url_base" cfg:"url_base"`
|
||||
} `json:"invite_emails"`
|
||||
Notifications struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Enabled struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value bool `json:"value" cfg:"enabled"`
|
||||
} `json:"enabled" cfg:"enabled"`
|
||||
ExpiryHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"expiry_html"`
|
||||
} `json:"expiry_html" cfg:"expiry_html"`
|
||||
ExpiryText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"expiry_text"`
|
||||
} `json:"expiry_text" cfg:"expiry_text"`
|
||||
CreatedHtml struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"created_html"`
|
||||
} `json:"created_html" cfg:"created_html"`
|
||||
CreatedText struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"created_text"`
|
||||
} `json:"created_text" cfg:"created_text"`
|
||||
} `json:"notifications"`
|
||||
Mailgun struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
ApiUrl struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"api_url"`
|
||||
} `json:"api_url" cfg:"api_url"`
|
||||
ApiKey struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"api_key"`
|
||||
} `json:"api_key" cfg:"api_key"`
|
||||
} `json:"mailgun"`
|
||||
Smtp struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Encryption struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options []string `json:"options"`
|
||||
Value string `json:"value" cfg:"encryption"`
|
||||
} `json:"encryption" cfg:"encryption"`
|
||||
Server struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"server"`
|
||||
} `json:"server" cfg:"server"`
|
||||
Port struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value int `json:"value" cfg:"port"`
|
||||
} `json:"port" cfg:"port"`
|
||||
Password struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"password"`
|
||||
} `json:"password" cfg:"password"`
|
||||
} `json:"smtp"`
|
||||
Files struct{
|
||||
Order []string `json:"order"`
|
||||
Meta Metadata `json:"meta"`
|
||||
Invites struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"invites"`
|
||||
} `json:"invites" cfg:"invites"`
|
||||
Emails struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"emails"`
|
||||
} `json:"emails" cfg:"emails"`
|
||||
UserTemplate struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_template"`
|
||||
} `json:"user_template" cfg:"user_template"`
|
||||
UserConfiguration struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_configuration"`
|
||||
} `json:"user_configuration" cfg:"user_configuration"`
|
||||
UserDisplayprefs struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"user_displayprefs"`
|
||||
} `json:"user_displayprefs" cfg:"user_displayprefs"`
|
||||
CustomCss struct{
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required"`
|
||||
Restart bool `json:"requires_restart"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value" cfg:"custom_css"`
|
||||
} `json:"custom_css" cfg:"custom_css"`
|
||||
} `json:"files"`
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import json
|
||||
|
||||
with open("config-formatted.json", "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
indent = 0
|
||||
|
||||
|
||||
def writeln(ln):
|
||||
global indent
|
||||
if "}" in ln and "{" not in ln:
|
||||
indent -= 1
|
||||
s.write(("\t" * indent) + ln + "\n")
|
||||
if "{" in ln and "}" not in ln:
|
||||
indent += 1
|
||||
|
||||
|
||||
with open("configStruct.go", "w") as s:
|
||||
writeln("package main")
|
||||
writeln("")
|
||||
writeln("type Metadata struct{")
|
||||
writeln('Name string `json:"name"`')
|
||||
writeln('Description string `json:"description"`')
|
||||
writeln("}")
|
||||
writeln("")
|
||||
writeln("type Config struct{")
|
||||
if "order" in config:
|
||||
writeln('Order []string `json:"order"`')
|
||||
for section in [x for x in config.keys() if x != "order"]:
|
||||
title = "".join([x.title() for x in section.split("_")])
|
||||
writeln(title + " struct{")
|
||||
if "order" in config[section]:
|
||||
writeln('Order []string `json:"order"`')
|
||||
if "meta" in config[section]:
|
||||
writeln('Meta Metadata `json:"meta"`')
|
||||
for setting in [
|
||||
x for x in config[section].keys() if x != "order" and x != "meta"
|
||||
]:
|
||||
name = "".join([x.title() for x in setting.split("_")])
|
||||
writeln(name + " struct{")
|
||||
writeln('Name string `json:"name"`')
|
||||
writeln('Required bool `json:"required"`')
|
||||
writeln('Restart bool `json:"requires_restart"`')
|
||||
writeln('Description string `json:"description"`')
|
||||
writeln('Type string `json:"type"`')
|
||||
dt = config[section][setting]["type"]
|
||||
if dt == "select":
|
||||
dt = "string"
|
||||
writeln('Options []string `json:"options"`')
|
||||
elif dt == "number":
|
||||
dt = "int"
|
||||
elif dt != "bool":
|
||||
dt = "string"
|
||||
writeln(f'Value {dt} `json:"value" cfg:"{setting}"`')
|
||||
writeln("} " + f'`json:"{setting}" cfg:"{setting}"`')
|
||||
writeln("} " + f'`json:"{section}"`')
|
||||
writeln("}")
|
||||
358
css/base.css
@@ -1,25 +1,64 @@
|
||||
@import "a17t.css";
|
||||
@import "remixicon.css";
|
||||
@import "modal.css";
|
||||
@import "dark.css";
|
||||
@import "tooltip.css";
|
||||
@import "loader.css";
|
||||
@import "./modal.css";
|
||||
@import "./dark.css";
|
||||
@import "./tooltip.css";
|
||||
@import "./loader.css";
|
||||
@import "./fonts.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--border-width-default: 2px;
|
||||
--border-width-2: 3px;
|
||||
--border-width-4: 5px;
|
||||
--border-width-8: 8px;
|
||||
font-family: 'Hanken Grotesk', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
.light {
|
||||
--settings-section-button-filter: 90%;
|
||||
}
|
||||
|
||||
.body {
|
||||
.dark {
|
||||
--settings-section-button-filter: 80%;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-color: #101010;
|
||||
}
|
||||
|
||||
html:not(.dark) body {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.dark select, .dark option, .dark input {
|
||||
background: #202020;
|
||||
}
|
||||
|
||||
html:not(.dark) .card.\@low:not(.\~neutral):not(.\~positive):not(.\~urge):not(.\~warning):not(.\~info):not(.\~critical),
|
||||
html:not(.dark) .card.\@low:not(.\~neutral):not(.\~positive):not(.\~urge):not(.\~warning):not(.\~info):not(.\~critical) > * {
|
||||
/* Colors from ~neutral */
|
||||
--color-fill-high: #64748b;
|
||||
--color-fill-low: #e2e8f0;
|
||||
--color-content-high: #f8fafc;
|
||||
--color-content-low: #1e293b;
|
||||
--color-accent-high: #475569;
|
||||
--color-accent-low: #cbd5e1;
|
||||
--color-muted-high: #475569;
|
||||
--color-muted-low: #f1f5f9;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.light-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark-only {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.page-container {
|
||||
margin: 5% 20% 5% 20%;
|
||||
}
|
||||
@@ -27,63 +66,47 @@
|
||||
@media (max-width: 1100px) {
|
||||
.page-container {
|
||||
margin: 2%;
|
||||
margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
@media screen and (max-width: 1000px) {
|
||||
:root {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.table-responsive table {
|
||||
min-width: 660px;
|
||||
min-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
.chip.btn:hover:not([disabled]):not(.textarea),
|
||||
.chip.btn:focus:not([disabled]):not(.textarea) {
|
||||
filter: brightness(var(--button-filter-brightness,95%));
|
||||
}
|
||||
|
||||
.banner {
|
||||
margin: calc(-1 * var(--spacing-4,1rem));
|
||||
}
|
||||
|
||||
.banner.header {
|
||||
margin-bottom: var(--spacing-4,1rem);
|
||||
max-width: calc(100% + 2.2rem); /* no idea why this works */
|
||||
margin-left: -1.1rem;
|
||||
}
|
||||
|
||||
.banner.footer {
|
||||
margin-top: var(--spacing-4,1rem);
|
||||
padding: var(--spacing-4,1rem);
|
||||
}
|
||||
|
||||
div.card:contains(section.banner.footer) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.mb-half {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mt-half {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.ml-half {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.pb-1 {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.al {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -92,6 +115,18 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ac {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -100,20 +135,11 @@
|
||||
align-items: top;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-expand {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-row-group {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
@@ -133,6 +159,33 @@
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
p.sm,
|
||||
span.sm:not(.heading) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.col.sm {
|
||||
margin: .25rem;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.flex-form {
|
||||
flex: 1;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
@@ -142,14 +195,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fr {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
background-color: inherit; /* so we can use a17t code blocks */
|
||||
}
|
||||
|
||||
sup.\~critical, .text-critical {
|
||||
color: var(--color-critical-normal-content);
|
||||
}
|
||||
@@ -200,7 +245,7 @@ sup.\~critical, .text-critical {
|
||||
max-width: 40%;
|
||||
min-width: 10rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -252,15 +297,23 @@ sup.\~critical, .text-critical {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.middle {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-lp {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.block {
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -307,10 +360,8 @@ sup.\~critical, .text-critical {
|
||||
}
|
||||
|
||||
.settings-section-button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 2.5rem;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.settings-section-button:hover, .settings-section-button:focus {
|
||||
@@ -322,8 +373,6 @@ sup.\~critical, .text-critical {
|
||||
}
|
||||
|
||||
.settings-section-button.selected {
|
||||
background-color: var(--color-neutral-normal-fill);
|
||||
--buton-filter-brightness: var(--settings-section-button-filter);
|
||||
filter: brightness(var(--settings-section-button-filter)) !important;
|
||||
}
|
||||
|
||||
@@ -351,17 +400,28 @@ select, textarea {
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
html.dark textarea {
|
||||
background-color: #202020
|
||||
}
|
||||
|
||||
input {
|
||||
color: inherit;
|
||||
border: 0 solid var(--color-neutral-300);
|
||||
}
|
||||
|
||||
table {
|
||||
color: var(--color-content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
p.top {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#notification-box {
|
||||
@@ -370,3 +430,157 @@ p.top {
|
||||
bottom: 1rem;
|
||||
z-index: 16;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
.dropdown.over-top {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.dropdown-display.lg {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-display.above {
|
||||
top: auto;
|
||||
bottom: 115%;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
background-color: var(--color-content-high) !important;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.circle {
|
||||
height: 0.5rem;
|
||||
width: 0.5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.circle.\~urge {
|
||||
background-color: var(--color-urge-200);
|
||||
}
|
||||
|
||||
.markdown-box {
|
||||
max-height: 20rem;
|
||||
display: block;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
a:link:not(.lang-link):not(.\~urge) {
|
||||
color: var(--color-urge-200);
|
||||
}
|
||||
|
||||
a:visited:not(.lang-link):not(.\~urge) {
|
||||
color: var(--color-urge-100);
|
||||
}
|
||||
|
||||
a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
||||
color: var(--color-urge-200);
|
||||
}
|
||||
|
||||
a.button,
|
||||
a.button:link,
|
||||
a.button:visited,
|
||||
a.button:focus,
|
||||
a.buton:hover {
|
||||
color: var(--color-content) !important;
|
||||
}
|
||||
|
||||
|
||||
.link-center {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* .search {
|
||||
max-width: 15rem;
|
||||
min-width: 10rem;
|
||||
} */
|
||||
|
||||
td.img-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
span.img-circle.lg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
span.shield.img-circle {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
img.img-circle {
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table td.sm {
|
||||
padding-top: 0.1rem;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.table-inline {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.card:contains(section.banner.footer) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.card.sectioned {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.card.sectioned .section {
|
||||
padding: var(--spacing-4, 1rem);
|
||||
}
|
||||
|
||||
.button.discord.\@low {
|
||||
background-color: rgba(88, 101, 242,60%);
|
||||
}
|
||||
|
||||
.button.discord.\@low:not(.lang-link) {
|
||||
color: rgba(38, 51, 192, 90%);
|
||||
}
|
||||
|
||||
.pb-0i {
|
||||
padding-bottom: 0px !important
|
||||
}
|
||||
|
||||
.mx-0i {
|
||||
margin-left: 0px !important;
|
||||
margin-right: 0px !important
|
||||
}
|
||||
|
||||
.text-center-i {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
input[type="checkbox" i], [class^="ri-"], [class*=" ri-"], .ri-refresh-line:before, .modal-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.g-recaptcha {
|
||||
overflow: hidden;
|
||||
width: 296px;
|
||||
height: 72px;
|
||||
transform: scale(1.1);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.g-recaptcha iframe {
|
||||
margin: -2px 0px 0px -4px;
|
||||
}
|
||||
|
||||
87
css/dark.css
@@ -1,87 +0,0 @@
|
||||
.dark-theme {
|
||||
|
||||
--settings-section-button-filter: 110%;
|
||||
|
||||
--color-neutral-900: rgba(255, 255, 255, 0.87);
|
||||
--color-neutral-800: rgba(255, 255, 255, 0.8);
|
||||
--color-neutral-700: rgba(255, 255, 255, 0.73);
|
||||
--color-neutral-600: rgba(255, 255, 255, 0.66);
|
||||
--color-neutral-500: rgb(153, 153, 153);
|
||||
--color-neutral-400: #383838;
|
||||
--color-neutral-300: #303030;
|
||||
--color-neutral-200: #292929;
|
||||
--color-neutral-100: #242424;
|
||||
--color-neutral-50: #202020;
|
||||
--color-neutral-000: #101010;
|
||||
|
||||
--color-critical-900: #fef2f2;
|
||||
--color-critical-800: #fee2e2;
|
||||
--color-critical-700: #fecaca;
|
||||
--color-critical-600: #fca5a5;
|
||||
--color-critical-500: #f87171;
|
||||
--color-critical-400: #ef4444;
|
||||
--color-critical-300: #dc2626;
|
||||
--color-critical-200: #b91c1c;
|
||||
--color-critical-100: #991b1b;
|
||||
--color-critical-50: #7f1d1d;
|
||||
--color-critical-000: #441313;
|
||||
|
||||
--color-warning-900: #fffbeb;
|
||||
--color-warning-800: #fef3c7;
|
||||
--color-warning-700: #fde68a;
|
||||
--color-warning-600: #fcd34d;
|
||||
--color-warning-500: #fbbf24;
|
||||
--color-warning-400: #f59e0b;
|
||||
--color-warning-300: #d97706;
|
||||
--color-warning-200: #b45309;
|
||||
--color-warning-100: #92400e;
|
||||
--color-warning-50: #783900;
|
||||
--color-warning-000: #411e01;
|
||||
|
||||
--color-positive-900: #f0fdf4;
|
||||
--color-positive-800: #dcfce7;
|
||||
--color-positive-700: #bbf7d0;
|
||||
--color-positive-600: #86efac;
|
||||
--color-positive-500: #4ade80;
|
||||
--color-positive-400: #22c55e;
|
||||
--color-positive-300: #16a34a;
|
||||
--color-positive-200: #15803d;
|
||||
--color-positive-100: #166534;
|
||||
--color-positive-50: #14532d;
|
||||
--color-positive-000: #0f2e1b;
|
||||
|
||||
--color-urge-900: #e0ffff;
|
||||
--color-urge-800: #c0fbff;
|
||||
--color-urge-700: #a0f4ff;
|
||||
--color-urge-600: #80e9ff;
|
||||
--color-urge-500: #60dbfb;
|
||||
--color-urge-400: #40cbf3;
|
||||
--color-urge-300: #20b9e9;
|
||||
--color-urge-200: #00a4dc; /* tab buttons */
|
||||
--color-urge-100: #0054bc;
|
||||
--color-urge-50: #00169a;
|
||||
--color-urge-000: #050076;
|
||||
|
||||
--color-info-900: #f5f3ff;
|
||||
--color-info-800: #ede9fe;
|
||||
--color-info-700: #ddd6fe;
|
||||
--color-info-600: #c4b5fd;
|
||||
--color-info-500: #a78bfa;
|
||||
--color-info-400: #8b5cf6;
|
||||
--color-info-300: #7c3aed;
|
||||
--color-info-200: #6d28d9;
|
||||
--color-info-100: #5b21b6;
|
||||
--color-info-50: #4c1d95;
|
||||
--color-info-000: #240e44;
|
||||
|
||||
|
||||
--color-neutral-normal-content: #ffffff;
|
||||
}
|
||||
|
||||
.light-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dark-only {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
351
css/dark.js
Normal file
@@ -0,0 +1,351 @@
|
||||
var c = {
|
||||
inherit: 'inherit',
|
||||
current: 'currentColor',
|
||||
transparent: 'transparent',
|
||||
black: '#000',
|
||||
white: '#fff',
|
||||
d_neutral: {
|
||||
900: "rgba(255, 255, 255, 0.87)",
|
||||
800: "rgba(255, 255, 255, 0.8)",
|
||||
700: "rgba(255, 255, 255, 0.73)",
|
||||
600: "rgba(255, 255, 255, 0.66)",
|
||||
500: "rgb(153, 153, 153)",
|
||||
400: "#383838",
|
||||
300: "#303030",
|
||||
200: "#292929",
|
||||
100: "#242424",
|
||||
50: "#202020",
|
||||
000: "#101010"
|
||||
},
|
||||
d_critical: {
|
||||
900: "#fef2f2",
|
||||
800: "#fee2e2",
|
||||
700: "#fecaca",
|
||||
600: "#fca5a5",
|
||||
500: "#f87171",
|
||||
400: "#ef4444",
|
||||
300: "#dc2626",
|
||||
200: "#b91c1c",
|
||||
100: "#991b1b",
|
||||
50: "#7f1d1d",
|
||||
000: "#441313"
|
||||
},
|
||||
d_warning: {
|
||||
900: "#fffbeb",
|
||||
800: "#fef3c7",
|
||||
700: "#fde68a",
|
||||
600: "#fcd34d",
|
||||
500: "#fbbf24",
|
||||
400: "#f59e0b",
|
||||
300: "#d97706",
|
||||
200: "#b45309",
|
||||
100: "#92400e",
|
||||
50: "#783900",
|
||||
000: "#411e01"
|
||||
},
|
||||
d_positive: {
|
||||
900: "#f0fdf4",
|
||||
800: "#dcfce7",
|
||||
700: "#bbf7d0",
|
||||
600: "#86efac",
|
||||
500: "#4ade80",
|
||||
400: "#22c55e",
|
||||
300: "#16a34a",
|
||||
200: "#15803d",
|
||||
100: "#166534",
|
||||
50: "#14532d",
|
||||
000: "#0f2e1b"
|
||||
},
|
||||
d_urge: {
|
||||
900: "#e0ffff",
|
||||
800: "#c0fbff",
|
||||
700: "#a0f4ff",
|
||||
600: "#80e9ff",
|
||||
500: "#60dbfb",
|
||||
400: "#40cbf3",
|
||||
300: "#20b9e9",
|
||||
200: "#00a4dc",
|
||||
100: "#0054bc",
|
||||
50: "#00169a",
|
||||
000: "#050076"
|
||||
},
|
||||
d_info: {
|
||||
900: "#f5f3ff",
|
||||
800: "#ede9fe",
|
||||
700: "#ddd6fe",
|
||||
600: "#c4b5fd",
|
||||
500: "#a78bfa",
|
||||
400: "#8b5cf6",
|
||||
300: "#7c3aed",
|
||||
200: "#6d28d9",
|
||||
100: "#5b21b6",
|
||||
50: "#4c1d95",
|
||||
000: "#240e44"
|
||||
},
|
||||
slate: {
|
||||
50: '#f8fafc',
|
||||
100: '#f1f5f9',
|
||||
200: '#e2e8f0',
|
||||
300: '#cbd5e1',
|
||||
400: '#94a3b8',
|
||||
500: '#64748b',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1e293b',
|
||||
900: '#0f172a'
|
||||
},
|
||||
gray: {
|
||||
50: '#f9fafb',
|
||||
100: '#f3f4f6',
|
||||
200: '#e5e7eb',
|
||||
300: '#d1d5db',
|
||||
400: '#9ca3af',
|
||||
500: '#6b7280',
|
||||
600: '#4b5563',
|
||||
700: '#374151',
|
||||
800: '#1f2937',
|
||||
900: '#111827'
|
||||
},
|
||||
zinc: {
|
||||
50: '#fafafa',
|
||||
100: '#f4f4f5',
|
||||
200: '#e4e4e7',
|
||||
300: '#d4d4d8',
|
||||
400: '#a1a1aa',
|
||||
500: '#71717a',
|
||||
600: '#52525b',
|
||||
700: '#3f3f46',
|
||||
800: '#27272a',
|
||||
900: '#18181b'
|
||||
},
|
||||
neutral: {
|
||||
50: '#fafafa',
|
||||
100: '#f5f5f5',
|
||||
200: '#e5e5e5',
|
||||
300: '#d4d4d4',
|
||||
400: '#a3a3a3',
|
||||
500: '#737373',
|
||||
600: '#525252',
|
||||
700: '#404040',
|
||||
800: '#262626',
|
||||
900: '#171717'
|
||||
},
|
||||
stone: {
|
||||
50: '#fafaf9',
|
||||
100: '#f5f5f4',
|
||||
200: '#e7e5e4',
|
||||
300: '#d6d3d1',
|
||||
400: '#a8a29e',
|
||||
500: '#78716c',
|
||||
600: '#57534e',
|
||||
700: '#44403c',
|
||||
800: '#292524',
|
||||
900: '#1c1917'
|
||||
},
|
||||
red: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d'
|
||||
},
|
||||
orange: {
|
||||
50: '#fff7ed',
|
||||
100: '#ffedd5',
|
||||
200: '#fed7aa',
|
||||
300: '#fdba74',
|
||||
400: '#fb923c',
|
||||
500: '#f97316',
|
||||
600: '#ea580c',
|
||||
700: '#c2410c',
|
||||
800: '#9a3412',
|
||||
900: '#7c2d12'
|
||||
},
|
||||
amber: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f'
|
||||
},
|
||||
yellow: {
|
||||
50: '#fefce8',
|
||||
100: '#fef9c3',
|
||||
200: '#fef08a',
|
||||
300: '#fde047',
|
||||
400: '#facc15',
|
||||
500: '#eab308',
|
||||
600: '#ca8a04',
|
||||
700: '#a16207',
|
||||
800: '#854d0e',
|
||||
900: '#713f12'
|
||||
},
|
||||
lime: {
|
||||
50: '#f7fee7',
|
||||
100: '#ecfccb',
|
||||
200: '#d9f99d',
|
||||
300: '#bef264',
|
||||
400: '#a3e635',
|
||||
500: '#84cc16',
|
||||
600: '#65a30d',
|
||||
700: '#4d7c0f',
|
||||
800: '#3f6212',
|
||||
900: '#365314'
|
||||
},
|
||||
green: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d'
|
||||
},
|
||||
emerald: {
|
||||
50: '#ecfdf5',
|
||||
100: '#d1fae5',
|
||||
200: '#a7f3d0',
|
||||
300: '#6ee7b7',
|
||||
400: '#34d399',
|
||||
500: '#10b981',
|
||||
600: '#059669',
|
||||
700: '#047857',
|
||||
800: '#065f46',
|
||||
900: '#064e3b'
|
||||
},
|
||||
teal: {
|
||||
50: '#f0fdfa',
|
||||
100: '#ccfbf1',
|
||||
200: '#99f6e4',
|
||||
300: '#5eead4',
|
||||
400: '#2dd4bf',
|
||||
500: '#14b8a6',
|
||||
600: '#0d9488',
|
||||
700: '#0f766e',
|
||||
800: '#115e59',
|
||||
900: '#134e4a'
|
||||
},
|
||||
cyan: {
|
||||
50: '#ecfeff',
|
||||
100: '#cffafe',
|
||||
200: '#a5f3fc',
|
||||
300: '#67e8f9',
|
||||
400: '#22d3ee',
|
||||
500: '#06b6d4',
|
||||
600: '#0891b2',
|
||||
700: '#0e7490',
|
||||
800: '#155e75',
|
||||
900: '#164e63'
|
||||
},
|
||||
sky: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e'
|
||||
},
|
||||
blue: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a'
|
||||
},
|
||||
indigo: {
|
||||
50: '#eef2ff',
|
||||
100: '#e0e7ff',
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
400: '#818cf8',
|
||||
500: '#6366f1',
|
||||
600: '#4f46e5',
|
||||
700: '#4338ca',
|
||||
800: '#3730a3',
|
||||
900: '#312e81'
|
||||
},
|
||||
violet: {
|
||||
50: '#f5f3ff',
|
||||
100: '#ede9fe',
|
||||
200: '#ddd6fe',
|
||||
300: '#c4b5fd',
|
||||
400: '#a78bfa',
|
||||
500: '#8b5cf6',
|
||||
600: '#7c3aed',
|
||||
700: '#6d28d9',
|
||||
800: '#5b21b6',
|
||||
900: '#4c1d95'
|
||||
},
|
||||
purple: {
|
||||
50: '#faf5ff',
|
||||
100: '#f3e8ff',
|
||||
200: '#e9d5ff',
|
||||
300: '#d8b4fe',
|
||||
400: '#c084fc',
|
||||
500: '#a855f7',
|
||||
600: '#9333ea',
|
||||
700: '#7e22ce',
|
||||
800: '#6b21a8',
|
||||
900: '#581c87'
|
||||
},
|
||||
fuchsia: {
|
||||
50: '#fdf4ff',
|
||||
100: '#fae8ff',
|
||||
200: '#f5d0fe',
|
||||
300: '#f0abfc',
|
||||
400: '#e879f9',
|
||||
500: '#d946ef',
|
||||
600: '#c026d3',
|
||||
700: '#a21caf',
|
||||
800: '#86198f',
|
||||
900: '#701a75'
|
||||
},
|
||||
pink: {
|
||||
50: '#fdf2f8',
|
||||
100: '#fce7f3',
|
||||
200: '#fbcfe8',
|
||||
300: '#f9a8d4',
|
||||
400: '#f472b6',
|
||||
500: '#ec4899',
|
||||
600: '#db2777',
|
||||
700: '#be185d',
|
||||
800: '#9d174d',
|
||||
900: '#831843'
|
||||
},
|
||||
rose: {
|
||||
50: '#fff1f2',
|
||||
100: '#ffe4e6',
|
||||
200: '#fecdd3',
|
||||
300: '#fda4af',
|
||||
400: '#fb7185',
|
||||
500: '#f43f5e',
|
||||
600: '#e11d48',
|
||||
700: '#be123c',
|
||||
800: '#9f1239',
|
||||
900: '#881337'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = c;
|
||||
44
css/fonts.css
Normal file
@@ -0,0 +1,44 @@
|
||||
/* hanken-grotesk-regular - cyrillic-ext_latin_vietnamese */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* hanken-grotesk-500 - cyrillic-ext_latin_vietnamese */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* hanken-grotesk-500italic - cyrillic-ext_latin_vietnamese */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ *
|
||||
}
|
||||
|
||||
/* hanken-grotesk-700 - cyrillic-ext_latin_vietnamese */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
/* hanken-grotesk-700italic - cyrillic-ext_latin_vietnamese */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
.loader {
|
||||
height: auto;
|
||||
color: rgba(0, 0, 0, 0);
|
||||
color: rgba(0, 0, 0, 0) !important;
|
||||
}
|
||||
|
||||
.loader .dot {
|
||||
|
||||
@@ -10,56 +10,6 @@
|
||||
background-color: rgba(0,0,0,40%);
|
||||
}
|
||||
|
||||
.modal-shown {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes modal-hide {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
.modal-hiding {
|
||||
animation: modal-hide 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
@keyframes modal-content-show {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: -6rem;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
margin: 10% auto;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.modal-content.wide {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.modal-shown .modal-content {
|
||||
animation: modal-content-show 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.modal-content.wide {
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
.modal-content, .modal-content.wide {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
float: right;
|
||||
color: #aaa;
|
||||
|
||||
101
daemon.go
@@ -2,27 +2,110 @@ package main
|
||||
|
||||
import "time"
|
||||
|
||||
// clearEmails removes stored emails for users which no longer exist.
|
||||
// meant to be called with other such housekeeping functions, so assumes
|
||||
// the user cache is fresh.
|
||||
func (app *appContext) clearEmails() {
|
||||
app.debug.Println("Housekeeping: removing unused email addresses")
|
||||
emails := app.storage.GetEmails()
|
||||
for _, email := range emails {
|
||||
_, status, err := app.jf.UserByID(email.JellyfinID, false)
|
||||
if status == 200 && err != nil {
|
||||
continue
|
||||
}
|
||||
app.storage.DeleteEmailsKey(email.JellyfinID)
|
||||
}
|
||||
}
|
||||
|
||||
// clearDiscord does the same as clearEmails, but for Discord Users.
|
||||
func (app *appContext) clearDiscord() {
|
||||
app.debug.Println("Housekeeping: removing unused Discord IDs")
|
||||
discordUsers := app.storage.GetDiscord()
|
||||
for _, discordUser := range discordUsers {
|
||||
_, status, err := app.jf.UserByID(discordUser.JellyfinID, false)
|
||||
if status == 200 && err != nil {
|
||||
continue
|
||||
}
|
||||
app.storage.DeleteDiscordKey(discordUser.JellyfinID)
|
||||
}
|
||||
}
|
||||
|
||||
// clearMatrix does the same as clearEmails, but for Matrix Users.
|
||||
func (app *appContext) clearMatrix() {
|
||||
app.debug.Println("Housekeeping: removing unused Matrix IDs")
|
||||
matrixUsers := app.storage.GetMatrix()
|
||||
for _, matrixUser := range matrixUsers {
|
||||
_, status, err := app.jf.UserByID(matrixUser.JellyfinID, false)
|
||||
if status == 200 && err != nil {
|
||||
continue
|
||||
}
|
||||
app.storage.DeleteMatrixKey(matrixUser.JellyfinID)
|
||||
}
|
||||
}
|
||||
|
||||
// clearTelegram does the same as clearEmails, but for Telegram Users.
|
||||
func (app *appContext) clearTelegram() {
|
||||
app.debug.Println("Housekeeping: removing unused Telegram IDs")
|
||||
telegramUsers := app.storage.GetTelegram()
|
||||
for _, telegramUser := range telegramUsers {
|
||||
_, status, err := app.jf.UserByID(telegramUser.JellyfinID, false)
|
||||
if status == 200 && err != nil {
|
||||
continue
|
||||
}
|
||||
app.storage.DeleteTelegramKey(telegramUser.JellyfinID)
|
||||
}
|
||||
}
|
||||
|
||||
// https://bbengfort.github.io/snippets/2016/06/26/background-work-goroutines-timer.html THANKS
|
||||
|
||||
type repeater struct {
|
||||
type housekeepingDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
Interval time.Duration
|
||||
period time.Duration
|
||||
jobs []func(app *appContext)
|
||||
app *appContext
|
||||
}
|
||||
|
||||
func newRepeater(interval time.Duration, app *appContext) *repeater {
|
||||
return &repeater{
|
||||
func newInviteDaemon(interval time.Duration, app *appContext) *housekeepingDaemon {
|
||||
daemon := housekeepingDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
Interval: interval,
|
||||
period: interval,
|
||||
app: app,
|
||||
}
|
||||
daemon.jobs = []func(app *appContext){func(app *appContext) {
|
||||
app.debug.Println("Housekeeping: Checking for expired invites")
|
||||
app.checkInvites()
|
||||
}}
|
||||
|
||||
clearEmail := app.config.Section("email").Key("require_unique").MustBool(false)
|
||||
clearDiscord := app.config.Section("discord").Key("require_unique").MustBool(false)
|
||||
clearTelegram := app.config.Section("telegram").Key("require_unique").MustBool(false)
|
||||
clearMatrix := app.config.Section("matrix").Key("require_unique").MustBool(false)
|
||||
|
||||
if clearEmail || clearDiscord || clearTelegram || clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.jf.CacheExpiry = time.Now() })
|
||||
}
|
||||
|
||||
if clearEmail {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearEmails() })
|
||||
}
|
||||
if clearDiscord {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearDiscord() })
|
||||
}
|
||||
if clearTelegram {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearTelegram() })
|
||||
}
|
||||
if clearMatrix {
|
||||
daemon.jobs = append(daemon.jobs, func(app *appContext) { app.clearMatrix() })
|
||||
}
|
||||
|
||||
return &daemon
|
||||
}
|
||||
|
||||
func (rt *repeater) run() {
|
||||
func (rt *housekeepingDaemon) run() {
|
||||
rt.app.info.Println("Invite daemon started")
|
||||
for {
|
||||
select {
|
||||
@@ -33,16 +116,18 @@ func (rt *repeater) run() {
|
||||
break
|
||||
}
|
||||
started := time.Now()
|
||||
rt.app.storage.loadInvites()
|
||||
rt.app.debug.Println("Daemon: Checking invites")
|
||||
rt.app.checkInvites()
|
||||
|
||||
for _, job := range rt.jobs {
|
||||
job(rt.app)
|
||||
}
|
||||
|
||||
finished := time.Now()
|
||||
duration := finished.Sub(started)
|
||||
rt.period = rt.Interval - duration
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *repeater) shutdown() {
|
||||
func (rt *housekeepingDaemon) Shutdown() {
|
||||
rt.Stopped = true
|
||||
rt.ShutdownChannel <- "Down"
|
||||
<-rt.ShutdownChannel
|
||||
|
||||
720
discord.go
Normal file
@@ -0,0 +1,720 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dg "github.com/bwmarrin/discordgo"
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
type DiscordDaemon struct {
|
||||
Stopped bool
|
||||
ShutdownChannel chan string
|
||||
bot *dg.Session
|
||||
username string
|
||||
tokens map[string]VerifToken // Map of pins to tokens.
|
||||
verifiedTokens map[string]DiscordUser // Map of token pins to discord users.
|
||||
channelID, channelName, inviteChannelID, inviteChannelName string
|
||||
guildID string
|
||||
serverChannelName, serverName string
|
||||
users map[string]DiscordUser // Map of user IDs to users. Added to on first interaction, and loaded from app.storage.discord on start.
|
||||
roleID string
|
||||
app *appContext
|
||||
commandHandlers map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string)
|
||||
commandIDs []string
|
||||
}
|
||||
|
||||
func newDiscordDaemon(app *appContext) (*DiscordDaemon, error) {
|
||||
token := app.config.Section("discord").Key("token").String()
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("token was blank")
|
||||
}
|
||||
bot, err := dg.New("Bot " + token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dd := &DiscordDaemon{
|
||||
Stopped: false,
|
||||
ShutdownChannel: make(chan string),
|
||||
bot: bot,
|
||||
tokens: map[string]VerifToken{},
|
||||
verifiedTokens: map[string]DiscordUser{},
|
||||
users: map[string]DiscordUser{},
|
||||
app: app,
|
||||
roleID: app.config.Section("discord").Key("apply_role").String(),
|
||||
commandHandlers: map[string]func(s *dg.Session, i *dg.InteractionCreate, lang string){},
|
||||
commandIDs: []string{},
|
||||
}
|
||||
dd.commandHandlers[app.config.Section("discord").Key("start_command").MustString("start")] = dd.cmdStart
|
||||
dd.commandHandlers["lang"] = dd.cmdLang
|
||||
dd.commandHandlers["pin"] = dd.cmdPIN
|
||||
for _, user := range app.storage.GetDiscord() {
|
||||
dd.users[user.ID] = user
|
||||
}
|
||||
|
||||
return dd, nil
|
||||
}
|
||||
|
||||
// NewAuthToken generates an 8-character pin in the form "A1-2B-CD".
|
||||
func (d *DiscordDaemon) NewAuthToken() string {
|
||||
pin := genAuthToken()
|
||||
d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: ""}
|
||||
return pin
|
||||
}
|
||||
|
||||
// NewAssignedAuthToken generates an 8-character pin in the form "A1-2B-CD",
|
||||
// and assigns it for access only with the given Jellyfin ID.
|
||||
func (d *DiscordDaemon) NewAssignedAuthToken(id string) string {
|
||||
pin := genAuthToken()
|
||||
d.tokens[pin] = VerifToken{Expiry: time.Now().Add(VERIF_TOKEN_EXPIRY_SEC * time.Second), JellyfinID: id}
|
||||
return pin
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) NewUnknownUser(channelID, userID, discrim, username string) DiscordUser {
|
||||
user := DiscordUser{
|
||||
ChannelID: channelID,
|
||||
ID: userID,
|
||||
Username: username,
|
||||
Discriminator: discrim,
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) MustGetUser(channelID, userID, discrim, username string) DiscordUser {
|
||||
if user, ok := d.users[userID]; ok {
|
||||
return user
|
||||
}
|
||||
return d.NewUnknownUser(channelID, userID, discrim, username)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) run() {
|
||||
d.bot.AddHandler(d.messageHandler)
|
||||
|
||||
d.bot.AddHandler(d.commandHandler)
|
||||
|
||||
d.bot.Identify.Intents = dg.IntentsGuildMessages | dg.IntentsDirectMessages | dg.IntentsGuildMembers | dg.IntentsGuildInvites
|
||||
if err := d.bot.Open(); err != nil {
|
||||
d.app.err.Printf("Discord: Failed to start daemon: %v", err)
|
||||
return
|
||||
}
|
||||
// Wait for everything to populate, it's slow sometimes.
|
||||
for d.bot.State == nil {
|
||||
continue
|
||||
}
|
||||
for d.bot.State.User == nil {
|
||||
continue
|
||||
}
|
||||
d.username = d.bot.State.User.Username
|
||||
for d.bot.State.Guilds == nil {
|
||||
continue
|
||||
}
|
||||
// Choose the last guild (server), for now we don't really support multiple anyway
|
||||
d.guildID = d.bot.State.Guilds[len(d.bot.State.Guilds)-1].ID
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||
}
|
||||
d.serverChannelName = guild.Name
|
||||
d.serverName = guild.Name
|
||||
if channel := d.app.config.Section("discord").Key("channel").String(); channel != "" {
|
||||
d.channelName = channel
|
||||
d.serverChannelName += "/" + channel
|
||||
}
|
||||
if d.app.config.Section("discord").Key("provide_invite").MustBool(false) {
|
||||
if invChannel := d.app.config.Section("discord").Key("invite_channel").String(); invChannel != "" {
|
||||
d.inviteChannelName = invChannel
|
||||
}
|
||||
}
|
||||
defer d.deregisterCommands()
|
||||
defer d.bot.Close()
|
||||
|
||||
go d.registerCommands()
|
||||
|
||||
<-d.ShutdownChannel
|
||||
d.ShutdownChannel <- "Down"
|
||||
return
|
||||
}
|
||||
|
||||
// ListRoles returns a list of available (excluding bot and @everyone) roles in a guild as a list of containing an array of the guild ID and its name.
|
||||
func (d *DiscordDaemon) ListRoles() (roles [][2]string, err error) {
|
||||
var r []*dg.Role
|
||||
r, err = d.bot.GuildRoles(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get roles: %v", err)
|
||||
return
|
||||
}
|
||||
for _, role := range r {
|
||||
if role.Name != d.username && role.Name != "@everyone" {
|
||||
roles = append(roles, [2]string{role.ID, role.Name})
|
||||
}
|
||||
}
|
||||
// roles = make([][2]string, len(r))
|
||||
// for i, role := range r {
|
||||
// roles[i] = [2]string{role.ID, role.Name}
|
||||
// }
|
||||
return
|
||||
}
|
||||
|
||||
// ApplyRole applies the member role to the given user if set.
|
||||
func (d *DiscordDaemon) ApplyRole(userID string) error {
|
||||
if d.roleID == "" {
|
||||
return nil
|
||||
}
|
||||
return d.bot.GuildMemberRoleAdd(d.guildID, userID, d.roleID)
|
||||
}
|
||||
|
||||
// NewTempInvite creates an invite link, and returns the invite URL, as well as the URL for the server icon.
|
||||
func (d *DiscordDaemon) NewTempInvite(ageSeconds, maxUses int) (inviteURL, iconURL string) {
|
||||
var inv *dg.Invite
|
||||
var err error
|
||||
if d.inviteChannelName == "" {
|
||||
d.app.err.Println("Discord: Cannot create invite without channel specified in settings.")
|
||||
return
|
||||
}
|
||||
if d.inviteChannelID == "" {
|
||||
channels, err := d.bot.GuildChannels(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel list: %v", err)
|
||||
return
|
||||
}
|
||||
found := false
|
||||
for _, channel := range channels {
|
||||
// channel, err := d.bot.Channel(ch.ID)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Couldn't get channel: %v", err)
|
||||
// return
|
||||
// }
|
||||
if channel.Name == d.inviteChannelName {
|
||||
d.inviteChannelID = channel.ID
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
d.app.err.Printf("Discord: Couldn't find invite channel \"%s\"", d.inviteChannelName)
|
||||
return
|
||||
}
|
||||
}
|
||||
// channel, err := d.bot.Channel(d.inviteChannelID)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Couldn't get invite channel: %v", err)
|
||||
// return
|
||||
// }
|
||||
inv, err = d.bot.ChannelInviteCreate(d.inviteChannelID, dg.Invite{
|
||||
// Guild: d.bot.State.Guilds[len(d.bot.State.Guilds)-1],
|
||||
// Channel: channel,
|
||||
// Inviter: d.bot.State.User,
|
||||
MaxAge: ageSeconds,
|
||||
MaxUses: maxUses,
|
||||
Temporary: false,
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create invite: %v", err)
|
||||
return
|
||||
}
|
||||
inviteURL = "https://discord.gg/" + inv.Code
|
||||
guild, err := d.bot.Guild(d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get guild: %v", err)
|
||||
return
|
||||
}
|
||||
// FIXME: Fix CSS, and handle no icon
|
||||
iconURL = guild.IconURL("256")
|
||||
fmt.Println("GOT ICON", iconURL)
|
||||
return
|
||||
}
|
||||
|
||||
// RenderDiscordUsername returns String of discord username, with support for new discriminator-less versions.
|
||||
func RenderDiscordUsername[DcUser *dg.User | DiscordUser](user DcUser) string {
|
||||
u, ok := interface{}(user).(*dg.User)
|
||||
var discriminator, username string
|
||||
if ok {
|
||||
discriminator = u.Discriminator
|
||||
username = u.Username
|
||||
} else {
|
||||
u2 := interface{}(user).(DiscordUser)
|
||||
discriminator = u2.Discriminator
|
||||
username = u2.Username
|
||||
}
|
||||
|
||||
if discriminator == "0" {
|
||||
return "@" + username
|
||||
}
|
||||
return username + "#" + discriminator
|
||||
}
|
||||
|
||||
// Returns the user(s) roughly corresponding to the username (if they are in the guild).
|
||||
// if no discriminator (#xxxx) is given in the username and there are multiple corresponding users, a list of all matching users is returned.
|
||||
func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
||||
members, err := d.bot.GuildMembers(
|
||||
d.guildID,
|
||||
"",
|
||||
1000,
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get members: %v", err)
|
||||
return nil
|
||||
}
|
||||
hasDiscriminator := strings.Contains(username, "#")
|
||||
hasAt := strings.HasPrefix(username, "@")
|
||||
if hasAt {
|
||||
username = username[1:]
|
||||
}
|
||||
var users []*dg.Member
|
||||
for _, member := range members {
|
||||
if hasDiscriminator {
|
||||
if member.User.Username+"#"+member.User.Discriminator == username {
|
||||
return []*dg.Member{member}
|
||||
}
|
||||
}
|
||||
if hasAt {
|
||||
if member.User.Username == username && member.User.Discriminator == "0" {
|
||||
return []*dg.Member{member}
|
||||
}
|
||||
}
|
||||
if strings.Contains(member.User.Username, username) {
|
||||
users = append(users, member)
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) NewUser(ID string) (user DiscordUser, ok bool) {
|
||||
u, err := d.bot.User(ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get user: %v", err)
|
||||
return
|
||||
}
|
||||
user.ID = ID
|
||||
user.Username = u.Username
|
||||
user.Contact = true
|
||||
user.Discriminator = u.Discriminator
|
||||
channel, err := d.bot.UserChannelCreate(ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create DM channel: %v", err)
|
||||
return
|
||||
}
|
||||
user.ChannelID = channel.ID
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) Shutdown() {
|
||||
d.Stopped = true
|
||||
d.ShutdownChannel <- "Down"
|
||||
<-d.ShutdownChannel
|
||||
close(d.ShutdownChannel)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) registerCommands() {
|
||||
commands := []*dg.ApplicationCommand{
|
||||
{
|
||||
Name: d.app.config.Section("discord").Key("start_command").MustString("start"),
|
||||
Description: "Start the Discord linking process. The bot will send further instructions.",
|
||||
},
|
||||
{
|
||||
Name: "lang",
|
||||
Description: "Set the language for the bot.",
|
||||
Options: []*dg.ApplicationCommandOption{
|
||||
{
|
||||
Type: dg.ApplicationCommandOptionString,
|
||||
Name: "language",
|
||||
Description: "Language Name",
|
||||
Required: true,
|
||||
Choices: []*dg.ApplicationCommandOptionChoice{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "pin",
|
||||
Description: "Send PIN for Discord verification.",
|
||||
Options: []*dg.ApplicationCommandOption{
|
||||
{
|
||||
Type: dg.ApplicationCommandOptionString,
|
||||
Name: "pin",
|
||||
Description: "Verification PIN (e.g AB-CD-EF)",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
commands[1].Options[0].Choices = make([]*dg.ApplicationCommandOptionChoice, len(d.app.storage.lang.Telegram))
|
||||
i := 0
|
||||
for code := range d.app.storage.lang.Telegram {
|
||||
d.app.debug.Printf("Registering choice \"%s\":\"%s\"\n", d.app.storage.lang.Telegram[code].Meta.Name, code)
|
||||
commands[1].Options[0].Choices[i] = &dg.ApplicationCommandOptionChoice{
|
||||
Name: d.app.storage.lang.Telegram[code].Meta.Name,
|
||||
Value: code,
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// d.deregisterCommands()
|
||||
|
||||
d.commandIDs = make([]string, len(commands))
|
||||
// cCommands, err := d.bot.ApplicationCommandBulkOverwrite(d.bot.State.User.ID, d.guildID, commands)
|
||||
// if err != nil {
|
||||
// d.app.err.Printf("Discord: Cannot create commands: %v", err)
|
||||
// }
|
||||
for i, cmd := range commands {
|
||||
command, err := d.bot.ApplicationCommandCreate(d.bot.State.User.ID, d.guildID, cmd)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Cannot create command \"%s\": %v", cmd.Name, err)
|
||||
} else {
|
||||
d.app.debug.Printf("Discord: registered command \"%s\"", cmd.Name)
|
||||
d.commandIDs[i] = command.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) deregisterCommands() {
|
||||
existingCommands, err := d.bot.ApplicationCommands(d.bot.State.User.ID, d.guildID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get commands: %v", err)
|
||||
return
|
||||
}
|
||||
for _, cmd := range existingCommands {
|
||||
if err := d.bot.ApplicationCommandDelete(d.bot.State.User.ID, "", cmd.ID); err != nil {
|
||||
d.app.err.Printf("Failed to deregister command: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) commandHandler(s *dg.Session, i *dg.InteractionCreate) {
|
||||
if h, ok := d.commandHandlers[i.ApplicationCommandData().Name]; ok {
|
||||
if i.GuildID != "" && d.channelName != "" {
|
||||
if d.channelID == "" {
|
||||
channel, err := s.Channel(i.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||
d.channelName = ""
|
||||
}
|
||||
if channel.Name == d.channelName {
|
||||
d.channelID = channel.ID
|
||||
}
|
||||
}
|
||||
if d.channelID != i.ChannelID {
|
||||
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||
return
|
||||
}
|
||||
}
|
||||
if i.Interaction.Member.User.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
lang := d.app.storage.lang.chosenTelegramLang
|
||||
if user, ok := d.users[i.Interaction.Member.User.ID]; ok {
|
||||
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
|
||||
lang = user.Lang
|
||||
}
|
||||
}
|
||||
h(s, i, lang)
|
||||
}
|
||||
}
|
||||
|
||||
// cmd* methods handle slash-commands, msg* methods handle ! commands.
|
||||
|
||||
func (d *DiscordDaemon) cmdStart(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
channel, err := s.UserChannelCreate(i.Interaction.Member.User.ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
return
|
||||
}
|
||||
user := d.MustGetUser(channel.ID, i.Interaction.Member.User.ID, i.Interaction.Member.User.Discriminator, i.Interaction.Member.User.Username)
|
||||
d.users[i.Interaction.Member.User.ID] = user
|
||||
|
||||
content := d.app.storage.lang.Telegram[lang].Strings.get("discordStartMessage") + "\n"
|
||||
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessageDiscord", tmpl{"command": "/lang"})
|
||||
err = s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
// Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
Content: content,
|
||||
Flags: 64, // Ephemeral
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) cmdPIN(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
pin := i.ApplicationCommandData().Options[0].StringValue()
|
||||
user, ok := d.tokens[pin]
|
||||
if !ok || time.Now().After(user.Expiry) {
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
// Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
Content: d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||
Flags: 64, // Ephemeral
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
}
|
||||
delete(d.tokens, pin)
|
||||
return
|
||||
}
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
// Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
Content: d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||
Flags: 64, // Ephemeral
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", i.Interaction.Member.User.Username, err)
|
||||
}
|
||||
dcUser := d.users[i.Interaction.Member.User.ID]
|
||||
dcUser.JellyfinID = user.JellyfinID
|
||||
d.verifiedTokens[pin] = dcUser
|
||||
delete(d.tokens, pin)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) cmdLang(s *dg.Session, i *dg.InteractionCreate, lang string) {
|
||||
code := i.ApplicationCommandData().Options[0].StringValue()
|
||||
if _, ok := d.app.storage.lang.Telegram[code]; ok {
|
||||
var user DiscordUser
|
||||
for _, u := range d.app.storage.GetDiscord() {
|
||||
if u.ID == i.Interaction.Member.User.ID {
|
||||
u.Lang = code
|
||||
lang = code
|
||||
d.app.storage.SetDiscordKey(u.JellyfinID, u)
|
||||
user = u
|
||||
break
|
||||
}
|
||||
}
|
||||
d.users[i.Interaction.Member.User.ID] = user
|
||||
err := s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
|
||||
// Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Type: dg.InteractionResponseChannelMessageWithSource,
|
||||
Data: &dg.InteractionResponseData{
|
||||
Content: d.app.storage.lang.Telegram[lang].Strings.template("languageSet", tmpl{"language": d.app.storage.lang.Telegram[lang].Meta.Name}),
|
||||
Flags: 64, // Ephemeral
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send reply: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) messageHandler(s *dg.Session, m *dg.MessageCreate) {
|
||||
if m.GuildID != "" && d.channelName != "" {
|
||||
if d.channelID == "" {
|
||||
channel, err := s.Channel(m.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Couldn't get channel, will monitor all: %v", err)
|
||||
d.channelName = ""
|
||||
}
|
||||
if channel.Name == d.channelName {
|
||||
d.channelID = channel.ID
|
||||
}
|
||||
}
|
||||
if d.channelID != m.ChannelID {
|
||||
d.app.debug.Printf("Discord: Ignoring message as not in specified channel")
|
||||
return
|
||||
}
|
||||
}
|
||||
if m.Author.ID == s.State.User.ID {
|
||||
return
|
||||
}
|
||||
sects := strings.Split(m.Content, " ")
|
||||
if len(sects) == 0 {
|
||||
return
|
||||
}
|
||||
lang := d.app.storage.lang.chosenTelegramLang
|
||||
if user, ok := d.users[m.Author.ID]; ok {
|
||||
if _, ok := d.app.storage.lang.Telegram[user.Lang]; ok {
|
||||
lang = user.Lang
|
||||
}
|
||||
}
|
||||
switch msg := sects[0]; msg {
|
||||
case "!" + d.app.config.Section("discord").Key("start_command").MustString("start"):
|
||||
d.msgStart(s, m, lang)
|
||||
case "!lang":
|
||||
d.msgLang(s, m, sects, lang)
|
||||
default:
|
||||
d.msgPIN(s, m, sects, lang)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) msgStart(s *dg.Session, m *dg.MessageCreate, lang string) {
|
||||
channel, err := s.UserChannelCreate(m.Author.ID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to create private channel with \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
user := d.MustGetUser(channel.ID, m.Author.ID, m.Author.Discriminator, m.Author.Username)
|
||||
d.users[m.Author.ID] = user
|
||||
|
||||
_, err = d.bot.ChannelMessageSendReply(m.ChannelID, d.app.storage.lang.Telegram[lang].Strings.get("discordDMs"), m.Reference())
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send reply to \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
|
||||
content := d.app.storage.lang.Telegram[lang].Strings.get("startMessage") + "\n"
|
||||
content += d.app.storage.lang.Telegram[lang].Strings.template("languageMessage", tmpl{"command": "!lang"})
|
||||
_, err = s.ChannelMessageSend(channel.ID, content)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) msgLang(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||
if len(sects) == 1 {
|
||||
list := "!lang <lang>\n"
|
||||
for code := range d.app.storage.lang.Telegram {
|
||||
list += fmt.Sprintf("%s: %s\n", code, d.app.storage.lang.Telegram[code].Meta.Name)
|
||||
}
|
||||
_, err := s.ChannelMessageSendReply(
|
||||
m.ChannelID,
|
||||
list,
|
||||
m.Reference(),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if _, ok := d.app.storage.lang.Telegram[sects[1]]; ok {
|
||||
var user DiscordUser
|
||||
for _, u := range d.app.storage.GetDiscord() {
|
||||
if u.ID == m.Author.ID {
|
||||
u.Lang = sects[1]
|
||||
d.app.storage.SetDiscordKey(u.JellyfinID, u)
|
||||
user = u
|
||||
break
|
||||
}
|
||||
}
|
||||
d.users[m.Author.ID] = user
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) msgPIN(s *dg.Session, m *dg.MessageCreate, sects []string, lang string) {
|
||||
if _, ok := d.users[m.Author.ID]; ok {
|
||||
channel, err := s.Channel(m.ChannelID)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to get channel: %v", err)
|
||||
return
|
||||
}
|
||||
if channel.Type != dg.ChannelTypeDM {
|
||||
d.app.debug.Println("Discord: Ignoring message as not a DM")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
d.app.debug.Println("Discord: Ignoring message as user was not found")
|
||||
return
|
||||
}
|
||||
user, ok := d.tokens[sects[0]]
|
||||
if !ok || time.Now().After(user.Expiry) {
|
||||
_, err := s.ChannelMessageSend(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("invalidPIN"),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
delete(d.tokens, sects[0])
|
||||
return
|
||||
}
|
||||
_, err := s.ChannelMessageSend(
|
||||
m.ChannelID,
|
||||
d.app.storage.lang.Telegram[lang].Strings.get("pinSuccess"),
|
||||
)
|
||||
if err != nil {
|
||||
d.app.err.Printf("Discord: Failed to send message to \"%s\": %v", m.Author.Username, err)
|
||||
}
|
||||
dcUser := d.users[m.Author.ID]
|
||||
dcUser.JellyfinID = user.JellyfinID
|
||||
d.verifiedTokens[sects[0]] = dcUser
|
||||
delete(d.tokens, sects[0])
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||
channels := make([]string, len(userID))
|
||||
for i, id := range userID {
|
||||
channel, err := d.bot.UserChannelCreate(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
channels[i] = channel.ID
|
||||
}
|
||||
return d.Send(message, channels...)
|
||||
}
|
||||
|
||||
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
||||
msg := ""
|
||||
var embeds []*dg.MessageEmbed
|
||||
if message.Markdown != "" {
|
||||
msg, embeds = StripAltText(message.Markdown, true)
|
||||
} else {
|
||||
msg = message.Text
|
||||
}
|
||||
for _, id := range channelID {
|
||||
var err error
|
||||
if len(embeds) != 0 {
|
||||
_, err = d.bot.ChannelMessageSendComplex(
|
||||
id,
|
||||
&dg.MessageSend{
|
||||
Content: msg,
|
||||
Embed: embeds[0],
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 1; i < len(embeds); i++ {
|
||||
_, err := d.bot.ChannelMessageSendEmbed(id, embeds[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err := d.bot.ChannelMessageSend(
|
||||
id,
|
||||
msg,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserVerified returns whether or not a token with the given PIN has been verified, and the user itself.
|
||||
func (d *DiscordDaemon) UserVerified(pin string) (user DiscordUser, ok bool) {
|
||||
user, ok = d.verifiedTokens[pin]
|
||||
// delete(d.verifiedTokens, pin)
|
||||
return
|
||||
}
|
||||
|
||||
// AssignedUserVerified returns whether or not a user with the given PIN has been verified, and the token itself.
|
||||
// Returns false if the given Jellyfin ID does not match the one in the user.
|
||||
func (d *DiscordDaemon) AssignedUserVerified(pin string, jfID string) (user DiscordUser, ok bool) {
|
||||
user, ok = d.verifiedTokens[pin]
|
||||
if ok && user.JellyfinID != jfID {
|
||||
ok = false
|
||||
}
|
||||
// delete(d.verifiedUsers, pin)
|
||||
return
|
||||
}
|
||||
|
||||
// UserExists returns whether or not a user with the given ID exists.
|
||||
func (d *DiscordDaemon) UserExists(id string) bool {
|
||||
c, err := d.app.storage.db.Count(&DiscordUser{}, badgerhold.Where("ID").Eq(id))
|
||||
return err != nil || c > 0
|
||||
}
|
||||
|
||||
// DeleteVerifiedUser removes the token with the given PIN.
|
||||
func (d *DiscordDaemon) DeleteVerifiedUser(pin string) {
|
||||
delete(d.verifiedTokens, pin)
|
||||
}
|
||||
113
exit.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robert-nix/ansihtml"
|
||||
)
|
||||
|
||||
// https://gist.github.com/swdunlop/9629168
|
||||
func identifyPanic() string {
|
||||
var name, file string
|
||||
var line int
|
||||
var pc [16]uintptr
|
||||
|
||||
n := runtime.Callers(4, pc[:])
|
||||
for _, pc := range pc[:n] {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
file, line = fn.FileLine(pc)
|
||||
name = fn.Name()
|
||||
if !strings.HasPrefix(name, "runtime.") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case name != "":
|
||||
return fmt.Sprintf("%v:%v", name, line)
|
||||
case file != "":
|
||||
return fmt.Sprintf("%v:%v", file, line)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("pc:%x", pc)
|
||||
}
|
||||
|
||||
// OpenFile attempts to open a given file in the appropriate GUI application.
|
||||
func OpenFile(fpath string) (err error) {
|
||||
switch PLATFORM {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", fpath).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", fpath).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", fpath).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unknown os")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Exit dumps the last 100 lines of output to a crash file in /tmp (or equivalent), and generates a prettier HTML file containing it that is opened in the browser if possible.
|
||||
func Exit(err interface{}) {
|
||||
tmpl, err2 := template.ParseFS(localFS, "html/crash.html", "html/header.html")
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
logCache := lineCache.String()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
logCache += "\n" + fmt.Sprint(err)
|
||||
}
|
||||
logCache += "\n" + string(debug.Stack())
|
||||
sanitized := sanitizeLog(logCache)
|
||||
data := map[string]interface{}{
|
||||
"Log": logCache,
|
||||
"SanitizedLog": sanitized,
|
||||
}
|
||||
if err != nil {
|
||||
data["Err"] = fmt.Sprintf("%s %v", identifyPanic(), err)
|
||||
}
|
||||
// Use dashes for time rather than colons for Windows
|
||||
fpath := filepath.Join(temp, "jfa-go-crash-"+time.Now().Local().Format("2006-01-02T15-04-05"))
|
||||
err2 = os.WriteFile(fpath+".txt", []byte(logCache), 0666)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to write crash dump file: %v", err2)
|
||||
}
|
||||
log.Printf("\n------\nA crash report has been saved to \"%s\".\n------", fpath+".txt")
|
||||
|
||||
// Render ANSI colors to HTML
|
||||
data["Log"] = template.HTML(string(ansihtml.ConvertToHTML([]byte(data["Log"].(string)))))
|
||||
data["SanitizedLog"] = template.HTML(string(ansihtml.ConvertToHTML([]byte(data["SanitizedLog"].(string)))))
|
||||
data["Err"] = template.HTML(string(ansihtml.ConvertToHTML([]byte(data["Err"].(string)))))
|
||||
|
||||
f, err2 := os.OpenFile(fpath+".html", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to open crash dump file: %v", err2)
|
||||
}
|
||||
defer f.Close()
|
||||
err2 = tmpl.Execute(f, data)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Failed to execute template: %v", err2)
|
||||
}
|
||||
if err := OpenFile(fpath + ".html"); err != nil {
|
||||
log.Printf("Failed to open browser, trying text file...")
|
||||
OpenFile(fpath + ".txt")
|
||||
}
|
||||
if TRAY {
|
||||
QuitTray()
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
51
external.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// +build external
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const binaryType = "external"
|
||||
|
||||
var localFS dirFS
|
||||
var langFS dirFS
|
||||
|
||||
// When using os.DirFS, even on Windows the separator seems to be '/'.
|
||||
// func FSJoin(elem ...string) string { return filepath.Join(elem...) }
|
||||
func FSJoin(elem ...string) string {
|
||||
sep := "/"
|
||||
if strings.Contains(elem[0], "\\") {
|
||||
sep = "\\"
|
||||
}
|
||||
path := ""
|
||||
for _, el := range elem {
|
||||
path += el + sep
|
||||
}
|
||||
return strings.TrimSuffix(path, sep)
|
||||
}
|
||||
|
||||
type dirFS string
|
||||
|
||||
func (dir dirFS) Open(name string) (fs.File, error) {
|
||||
return os.Open(string(dir) + "/" + name)
|
||||
}
|
||||
|
||||
func (dir dirFS) ReadFile(name string) ([]byte, error) {
|
||||
return os.ReadFile(string(dir) + "/" + name)
|
||||
}
|
||||
|
||||
func (dir dirFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||
return os.ReadDir(string(dir) + "/" + name)
|
||||
}
|
||||
|
||||
func loadFilesystems() {
|
||||
log.Println("Using external storage")
|
||||
executable, _ := os.Executable()
|
||||
localFS = dirFS(filepath.Join(filepath.Dir(executable), "data"))
|
||||
langFS = dirFS(filepath.Join(filepath.Dir(executable), "data", "lang"))
|
||||
}
|
||||
161
go.mod
@@ -1,54 +1,129 @@
|
||||
module github.com/hrfee/jfa-go
|
||||
|
||||
go 1.14
|
||||
go 1.20
|
||||
|
||||
replace github.com/hrfee/jfa-go/docs => ./docs
|
||||
|
||||
replace github.com/hrfee/jfa-go/jfapi => ./jfapi
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ./common
|
||||
|
||||
replace github.com/hrfee/jfa-go/ombi => ./ombi
|
||||
|
||||
replace github.com/hrfee/jfa-go/logger => ./logger
|
||||
|
||||
replace github.com/hrfee/jfa-go/linecache => ./linecache
|
||||
|
||||
replace github.com/hrfee/jfa-go/api => ./api
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/gin-contrib/pprof v1.3.0
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/spec v0.20.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/gofrs/uuid v3.3.0+incompatible // indirect
|
||||
github.com/golang/protobuf v1.4.3
|
||||
github.com/google/uuid v1.1.2 // indirect
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/jfapi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20201112212552-b6f3cd7c1f71
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible
|
||||
github.com/json-iterator/go v1.1.10 // indirect
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
|
||||
github.com/swaggo/gin-swagger v1.3.0
|
||||
github.com/swaggo/swag v1.7.0 // indirect
|
||||
github.com/ugorji/go v1.2.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
github.com/bwmarrin/discordgo v0.27.1
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/gin-contrib/pprof v1.4.0
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
||||
github.com/hrfee/jfa-go/common v0.0.0-20230421170108-d800b97f69b6
|
||||
github.com/hrfee/jfa-go/docs v0.0.0-20230421170108-d800b97f69b6
|
||||
github.com/hrfee/jfa-go/linecache v0.0.0-20230421170108-d800b97f69b6
|
||||
github.com/hrfee/jfa-go/logger v0.0.0-20230421170108-d800b97f69b6
|
||||
github.com/hrfee/jfa-go/ombi v0.0.0-20230421170108-d800b97f69b6
|
||||
github.com/hrfee/mediabrowser v0.3.8
|
||||
github.com/itchyny/timefmt-go v0.1.5
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
github.com/mailgun/mailgun-go/v4 v4.9.0
|
||||
github.com/robert-nix/ansihtml v1.0.1
|
||||
github.com/steambap/captcha v1.4.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
maunium.net/go/mautrix v0.15.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.1 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 // indirect
|
||||
github.com/getlantern/errors v1.0.3 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.1 // indirect
|
||||
github.com/go-stack/stack v1.8.1 // indirect
|
||||
github.com/go-test/deep v1.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v2.0.0+incompatible // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.13.1 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rs/zerolog v1.29.1 // indirect
|
||||
github.com/swaggo/swag v1.16.1 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tidwall/gjson v1.14.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/tools v0.9.3 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
maunium.net/go/maulogger/v2 v2.4.1 // indirect
|
||||
)
|
||||
|
||||
632
go.sum
@@ -1,123 +1,179 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.1 h1:zaX53IRg7ycxVlkd5pYdCeFp1FynD6qBGQoQql3R3Hk=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:M88ob4TyDnEqNuL3PgsE/p3bDujfspnulR+0dQWNYZs=
|
||||
github.com/emersion/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:buzQsO8HHkZX2Q45fdfGH1xejPjuDQaXH8btcYMFzPM=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA=
|
||||
github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.1/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/errors v1.0.3 h1:Ne4Ycj7NI1BtSyAfVeAT/DNoxz7/S2BUc3L2Ht1YSHE=
|
||||
github.com/getlantern/errors v1.0.3/go.mod h1:m8C7H1qmouvsGpwQqk/6NUpIVMpfzUPn608aBZDYV04=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 h1:NlQedYmPI3pRAXJb+hLVVDGqfvvXGRPV8vp7XOjKAZ0=
|
||||
github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65/go.mod h1:+ZU1h+iOVqWReBpky6d5Y2WL0sF2Llxu+QcxJFs2+OU=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc h1:sue+aeVx7JF5v36H1HfvcGFImLpSD5goj8d+MitovDU=
|
||||
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc/go.mod h1:D9RWpXy/EFPYxiKUURo2TB8UBosbqkiLhttRrZYtvqM=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 h1:cSrD9ryDfTV2yaur9Qk3rHYD414j3Q1rl7+L0AylxrE=
|
||||
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770/go.mod h1:GOQsoDnEHl6ZmNIL+5uVo+JWRFWozMEp18Izcb++H+A=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20220713155959-1315d978fff7/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c h1:qcPAzA1ZDnwx618jAgQmxo6UvJkw2SkM1L4ofncmEhI=
|
||||
github.com/getlantern/ops v0.0.0-20230519221840-1283e026181c/go.mod h1:g2ueCncOwWenlAr56Fh90FwsACkelqqtFUDLAHg1mng=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w=
|
||||
github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0=
|
||||
github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 h1:xLG16iua01X7Gzms9045s2Y2niNpvSY/Zb1oBwgNYZY=
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1 h1:plQYoJeO9lI8Ag0xZy7dDF8FMwIOHsQylKjcclknvIc=
|
||||
github.com/gin-contrib/static v0.0.0-20200815103939-31fb0c56a3d1/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e h1:8bZpGwoPxkaivQPrAbWl+7zjjUcbFUnYp7yQcx2r2N0=
|
||||
github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e/go.mod h1:VhW/Ch/3FhimwZb8Oj+qJmdMmoB8r7lmJ5auRjm50oQ=
|
||||
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
|
||||
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||
github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
|
||||
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||
github.com/go-openapi/jsonreference v0.19.4 h1:3Vw+rh13uq2JFNxgnMTGE1rnoieU9FmyE1gvnyylsYg=
|
||||
github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo=
|
||||
github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
||||
github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc=
|
||||
github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
|
||||
github.com/go-openapi/spec v0.19.10 h1:pcNevfYytLaOQuTju0wm6OqcqU/E/pRwuSGigrLTI28=
|
||||
github.com/go-openapi/spec v0.19.10/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
|
||||
github.com/go-openapi/spec v0.19.12 h1:OO9WrvhDwtiMY/Opr1j1iFZzirI3JW4/bxNFRcntAr4=
|
||||
github.com/go-openapi/spec v0.19.12/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
github.com/go-openapi/spec v0.19.13 h1:AcZVcWsrfW7LqyHKVbTZYpFF7jQcMxmAsWrw2p/b9ew=
|
||||
github.com/go-openapi/spec v0.19.13/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
github.com/go-openapi/spec v0.19.14 h1:r4fbYFo6N4ZelmSX8G6p+cv/hZRXzcuqQIADGT1iNKM=
|
||||
github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
|
||||
github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86I=
|
||||
github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
|
||||
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
|
||||
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE=
|
||||
github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
|
||||
github.com/go-openapi/swag v0.19.10 h1:A1SWXruroGP15P1sOiegIPbaKio+G9N5TwWTFaVPmAU=
|
||||
github.com/go-openapi/swag v0.19.10/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
||||
github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc=
|
||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
|
||||
github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI=
|
||||
github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
|
||||
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.4.0 h1:72qIR/m8ybvL8L5TIyfgrigqkrw7kVYAvjEvpT85l70=
|
||||
github.com/go-playground/validator/v10 v10.4.0/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
|
||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@@ -125,175 +181,262 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI=
|
||||
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible h1:CL0ooBNfbNyJTJATno+m0h+zM5bW6v7fKlboKUGP/dI=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200917010138-e1c00e156980+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk=
|
||||
github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9 h1:GQE1iatYDRrIidq4Zf/9ZzKWyrTk2sXOYc1JADbkAjQ=
|
||||
github.com/knz/strtime v0.0.0-20200318182718-be999391ffa9/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius=
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e h1:ViPE0JEOvtw5I0EGUiFSr2VNKGNU+3oBT+oHbDXHbxk=
|
||||
github.com/knz/strtime v0.0.0-20200924090105-187c67f2bf5e/go.mod h1:4ZxfWkxwtc7dBeifERVVWRy9F9rTU9p0yCDgeCtlius=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
|
||||
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
||||
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
||||
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lithammer/shortuuid v1.0.0 h1:kdcbvjGVEgqeVeDIRtnANOi/F6ftbKrtbxY+cjQmK1Q=
|
||||
github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/mailgun/mailgun-go v1.1.1 h1:mjMcm4qz+SbjAYbGJ6DKROViKtO5S0YjpuOUxQfdr2A=
|
||||
github.com/mailgun/mailgun-go v2.0.0+incompatible h1:0FoRHWwMUctnd8KIR3vtZbqdfjpIMxOZgcSa51s8F8o=
|
||||
github.com/mailgun/mailgun-go/v4 v4.1.3 h1:KLa5EZaOMMeyvY/lfAhWxv9ealB3mtUsMz0O9XmTtP0=
|
||||
github.com/mailgun/mailgun-go/v4 v4.1.3/go.mod h1:R9kHUQBptF4iSEjhriCQizplCDwrnDShy8w/iPiOfaM=
|
||||
github.com/mailgun/mailgun-go/v4 v4.2.0 h1:AAt7TwR98Pog7zAYK61SW7ikykFFmCovtix3vvS2cK4=
|
||||
github.com/mailgun/mailgun-go/v4 v4.2.0/go.mod h1:R9kHUQBptF4iSEjhriCQizplCDwrnDShy8w/iPiOfaM=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0 h1:9nAF7LI3k6bfDPbMZQMMl63Q8/vs+dr1FUN8eR1XMhk=
|
||||
github.com/mailgun/mailgun-go/v4 v4.3.0/go.mod h1:fWuBI2iaS/pSSyo6+EBpHjatQO3lV8onwqcRy7joSJI=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailgun/mailgun-go/v4 v4.9.0 h1:wRbxvVQ5QObFewLxc1uVvipA16D8gxeiO+cBOca51Iw=
|
||||
github.com/mailgun/mailgun-go/v4 v4.9.0/go.mod h1:FJlF9rI5cQT+mrwujtJjPMbIVy3Ebor9bKTVsJ0QU40=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mailru/easyjson v0.7.2 h1:V9ecaZWDYm7v9uJ15RZD6DajMu5sE0hdep0aoDwT9g4=
|
||||
github.com/mailru/easyjson v0.7.2/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.3 h1:M6wcO9gFHCIPynXGu4iA+NMs//FCgFUWR2jxqV3/+Xk=
|
||||
github.com/mailru/easyjson v0.7.3/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858 h1:lgbJiJQx8bXo+eM88AFdd0VxUvaTLzCBXpK+H9poJ+Y=
|
||||
github.com/pdrum/swagger-automation v0.0.0-20190629163613-c8c7c80ba858/go.mod h1:y02HeaN0visd95W6cEX2NXDv5sCwyqfzucWTdDGEwYY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
|
||||
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/steambap/captcha v1.4.1 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P0=
|
||||
github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
||||
github.com/swaggo/gin-swagger v1.2.0 h1:YskZXEiv51fjOMTsXrOetAjrMDfFaXD79PEoQBOe2W0=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI=
|
||||
github.com/swaggo/gin-swagger v1.3.0 h1:eOmp7r57oUgZPw2dJOjcGNMse9cvXcI4tTqBcnZtPsI=
|
||||
github.com/swaggo/gin-swagger v1.3.0/go.mod h1:oy1BRA6WvgtCp848lhxce7BnWH4C8Bxa0m5SkWx+cS0=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y=
|
||||
github.com/swaggo/swag v1.6.7 h1:e8GC2xDllJZr3omJkm9YfmK0Y56+rMO3cg0JBKNz09s=
|
||||
github.com/swaggo/swag v1.6.7/go.mod h1:xDhTyuFIujYiN3DKWC/H/83xcfHp+UE/IzWWampG7Zc=
|
||||
github.com/swaggo/swag v1.6.8 h1:z3ZNcpJs/NLMpZcKqXUsBELmmY2Ocy09JXKx5gu3L4M=
|
||||
github.com/swaggo/swag v1.6.8/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI=
|
||||
github.com/swaggo/swag v1.6.9 h1:BukKRwZjnEcUxQt7Xgfrt9fpav0hiWw9YimdNO9wssw=
|
||||
github.com/swaggo/swag v1.6.9/go.mod h1:a0IpNeMfGidNOcm2TsqODUh9JHdHu3kxDA0UlGbBKjI=
|
||||
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
|
||||
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
|
||||
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
|
||||
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e/go.mod h1:/Seq5xGNo8jLhSbDX3jdbeZrp4yFIpQ6/7n4TjziEWs=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2 h1:83OLY/NFnEaMnHEPd84bYtkLipVkjTsMbzQRYbk47g4=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2/go.mod h1:rh6RyXLQFsvrvcKondPQQFZnNovpRzu+gS0FlLxYuHY=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.1.9 h1:SObrQTaSuP8WOv2WNCj8gECiNSJIUvk3Q7N26c96Gws=
|
||||
github.com/ugorji/go v1.1.9/go.mod h1:chLrngdsg43geAaeId+nXO57YsDdl5OZqd/QtBiD19g=
|
||||
github.com/ugorji/go v1.1.13/go.mod h1:jxau1n+/wyTGLQoCkjok9r5zFa/FxT6eI5HiHKQszjc=
|
||||
github.com/ugorji/go v1.2.0 h1:6eXlzYLLwZwXroJx9NyqbYcbv/d93twiOzQLDewE6qM=
|
||||
github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.1.9 h1:J/7hhpkQwgypRNvaeh/T5gzJ2gEI/l8S3qyRrdEa1fA=
|
||||
github.com/ugorji/go/codec v1.1.9/go.mod h1:+SWgpdqOgdW5sBaiDfkHilQ1SxQ1hBkq/R+kHfL7Suo=
|
||||
github.com/ugorji/go/codec v1.1.13/go.mod h1:oNVt3Dq+FO91WNQ/9JnHKQP2QJxTzoN7wCBFCq1OeuU=
|
||||
github.com/ugorji/go/codec v1.2.0 h1:As6RccOIlbm9wHuWYMlB30dErcI+4WiKWsYsmPkyrUw=
|
||||
github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs=
|
||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw=
|
||||
github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE=
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0 h1:OANWU9jHZrVfBkNkvLf8Ww0fexwpQVF/v/5f96fFTLI=
|
||||
github.com/xhit/go-simple-mail/v2 v2.13.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
|
||||
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
|
||||
go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
|
||||
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -305,66 +448,71 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200923182212-328152dc79b1 h1:Iu68XRPd67wN4aRGGWwwq6bZo/25jR6uu52l/j2KkUE=
|
||||
golang.org/x/net v0.0.0-20200923182212-328152dc79b1/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728 h1:5wtQIAulKU5AbLQOkjxl32UufnIOqgBX72pS0AV14H0=
|
||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0 h1:5kGOVHlq0euqwzgTC9Vu15p6fV1Wi0ArVi8da2urnVg=
|
||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed h1:J22ig1FUekjjkmZUM7pTKixYm8DvrYsvrBZdunYeIuQ=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c h1:/h0vtH0PyU0xAoZJVcRw1k0Ng+U0JAy3QDiFmppIlIE=
|
||||
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -372,32 +520,15 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200923182640-463111b69878 h1:VUw1+Jf6KJPf82mbTQMia6HCnNMv2BbAipkEZ4KTcqQ=
|
||||
golang.org/x/tools v0.0.0-20200923182640-463111b69878/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20200924182824-0f1c53950d78 h1:3JUoxVhcskhsIDEc7vg0MUUEpmPPN5TfG+E97z/Fn90=
|
||||
golang.org/x/tools v0.0.0-20200924182824-0f1c53950d78/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20200929191002-f1e51e6b9437 h1:XSFqH8m531iIGazX5lrUC9j3slbwsZ1GFByqdUrLqmI=
|
||||
golang.org/x/tools v0.0.0-20200929191002-f1e51e6b9437/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06 h1:w9ail9jFLaySAm61Zjhciu0LQ5i8YTy2pimlNLx4uuk=
|
||||
golang.org/x/tools v0.0.0-20201008184944-d01b322e6f06/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201017001424-6003fad69a88 h1:ZB1XYzdDo7c/O48jzjMkvIjnC120Z9/CwgDWhePjQdQ=
|
||||
golang.org/x/tools v0.0.0-20201017001424-6003fad69a88/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9 h1:sEvmEcJVKBNUvgCUClbUQeHOAa9U0I2Ce1BooMvVCY4=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201103235415-b653051172e4 h1:Qe0EMgvVYb6tmJhJHljCj3gS96hvSTkGNaIzp/ivq10=
|
||||
golang.org/x/tools v0.0.0-20201103235415-b653051172e4/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201113202037-1643af1435f3 h1:7R7+wzd5VuLvCNyHZ/MG511kkoP/DBEzkbh8qUsFbY8=
|
||||
golang.org/x/tools v0.0.0-20201113202037-1643af1435f3/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b h1:Ych5r0Z6MLML1fgf5hTg9p5bV56Xqx9xv9hLgMBATWs=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e h1:t96dS3DO8DGjawSLJL/HIdz8CycAd2v07XxqB3UPTi0=
|
||||
golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee h1:5xKxdl/RhlelmSPaxyVeq5PYSmJ4H14yeQT58qP1F6o=
|
||||
golang.org/x/tools v0.0.0-20210104081019-d8d6ddbec6ee/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
|
||||
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -405,47 +536,56 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60=
|
||||
gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10=
|
||||
gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||
maunium.net/go/mautrix v0.15.2 h1:fUiVajeoOR92uJoSShHbCvh7uG6lDY4ZO4Mvt90LbjU=
|
||||
maunium.net/go/mautrix v0.15.2/go.mod h1:h4NwfKqE4YxGTLSgn/gawKzXAb2sF4qx8agL6QEFtGg=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>404 - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
<div class="page-container">
|
||||
<h1 class="heading">Page not found.</h1>
|
||||
<p class="content">
|
||||
{{ .contactMessage }}
|
||||
</p>
|
||||
<div class="card">
|
||||
<h1 class="heading">Page not found.</h1>
|
||||
<p class="content">
|
||||
{{ .contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
52
html/account-linking.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{{ if .discordEnabled }}
|
||||
<div id="modal-discord" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ .strings.linkDiscord }}</span>
|
||||
<p class="content mb-4"> {{ .discordSendPINMessage }}</p>
|
||||
<h1 class="text-center text-2xl mb-2 pin"></h1>
|
||||
<div class="row center">
|
||||
<a class="my-5 hover:underline">
|
||||
<span class="mr-2">{{ .strings.joinTheServer }}</span>
|
||||
<span id="discord-invite"></span>
|
||||
</a>
|
||||
</div>
|
||||
<span class="button ~info @low full-width center mt-4" id="discord-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .telegramEnabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
||||
<p class="text-center text-2xl mb-2 pin"></p>
|
||||
<a class="subheading link-center" href="{{ .telegramURL }}" target="_blank">
|
||||
<span class="shield ~info mr-4">
|
||||
<span class="icon">
|
||||
<i class="ri-telegram-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
@{{ .telegramUsername }}
|
||||
</a>
|
||||
<span class="button ~info @low full-width center mt-4" id="telegram-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content mb-4"> {{ .strings.matrixEnterUser }}</p>
|
||||
<input type="text" class="input ~neutral @high" placeholder="@user:riot.im" id="matrix-userid">
|
||||
<div class="subheading link-center mt-4">
|
||||
<span class="shield ~info mr-4">
|
||||
<span class="icon">
|
||||
<i class="ri-chat-3-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
{{ .matrixUser }}
|
||||
</div>
|
||||
<span class="button ~info @low full-width center mt-4" id="matrix-send">{{ .strings.submit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
736
html/admin.html
@@ -1,272 +1,644 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
<script>
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.emailEnabled = {{ .email_enabled }};
|
||||
window.emailEnabled = {{ .emailEnabled }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.usernamesEnabled = {{ .username }};
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
||||
window.language = "{{ .langName }}";
|
||||
window.jellyfinLogin = {{ .jellyfinLogin }};
|
||||
window.jfAdminOnly = {{ .jfAdminOnly }};
|
||||
window.jfAllowAll = {{ .jfAllowAll }};
|
||||
</script>
|
||||
{{ template "header.html" . }}
|
||||
<title>Admin - jfa-go</title>
|
||||
{{ template "header.html" . }}
|
||||
</head>
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
<div id="modal-login" class="modal">
|
||||
<form class="modal-content card" id="form-login" href="">
|
||||
<span class="heading">Login</span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="username" id="login-user">
|
||||
<input type="password" class="field input ~neutral !high mb-1" placeholder="password" id="login-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Login</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
{{ template "login-modal.html" . }}
|
||||
<div id="modal-add-user" class="modal">
|
||||
<form class="modal-content card" id="form-add-user" href="">
|
||||
<span class="heading">New User <span class="modal-close">×</span></span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="username" id="add-user-user">
|
||||
<input type="email" class="field input ~neutral !high mt-half mb-1" placeholder="email address">
|
||||
<input type="password" class="field input ~neutral !high mb-1" placeholder="password" id="add-user-password">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-user" href="">
|
||||
<span class="heading">{{ .strings.newUser }} <span class="modal-close">×</span></span>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="add-user-user">
|
||||
<input type="email" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.emailAddress }}">
|
||||
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="add-user-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.create }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-about" class="modal">
|
||||
<div class="modal-content content card">
|
||||
<span class="heading">About <span class="modal-close">×</span></span>
|
||||
<img src="/banner.svg" class="mt-1" alt="jfa-go banner">
|
||||
<p><i class="icon ri-github-fill"></i><a href="https://github.com/hrfee/jfa-go">jfa-go</a></p>
|
||||
<p>Version <span class="code monospace">{{ .version }}</span></p>
|
||||
<p>Commit <span class="code monospace">{{ .commit }}</span></p>
|
||||
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License.</a></p>
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-1/3 content card">
|
||||
<img src="{{ .urlBase }}/banner.svg" class="banner header" alt="jfa-go banner">
|
||||
<span class="heading"><span class="modal-close">×</span></span>
|
||||
<p>{{ .strings.version }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .version }}</span></p>
|
||||
<p>{{ .strings.commitNoun }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .commit }}</span></p>
|
||||
<p>{{ .strings.buildTime }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .buildTime }}</span></p>
|
||||
<p>{{ .strings.builtBy }} <span class="text-black dark:text-white font-mono bg-inherit">{{ .builtBy }}</span></p>
|
||||
<div class="row col flex">
|
||||
<a class="button ~neutral mr-2 mt-4 mb-4 lang-link" href="https://github.com/hrfee/jfa-go"><i class="ri-github-line mr-2"></i>github</a>
|
||||
<a class="button ~urge mt-4 mb-4 mr-2 lang-link" href="https://wiki.jfa-go.com">wiki/docs</a>
|
||||
<a class="button ~positive mt-4 mb-4 mr-2 lang-link" href="https://weblate.jfa-go.com">translation</a>
|
||||
<div class="dropdown mr-2" tabindex="0">
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~info mt-4 mb-4 dropdown-button lang-link">
|
||||
<i class="ri-hand-heart-line mr-2"></i>
|
||||
donate
|
||||
<span class="ml-2 chev"></span>
|
||||
</a>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<a href="https://github.com/sponsors/hrfee" target="_blank" class="button ~neutral mb-2 w-100 lang-link">GitHub</a>
|
||||
<a href="https://ko-fi.com/hrfee" target="_blank" class="button ~neutral mb-2 w-100 lang-link">Ko-fi</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="button ~urge mt-4 mb-4 @low discord lang-link" href="https://discord.com/invite/MrtvuQmyhP" target="_blank"><i class="ri-discord-line mr-2"></i>discord</a>
|
||||
</div>
|
||||
<p><a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE">Available under the MIT License. Font "Hanken Grotesk" available under SIL OFL 1.1 License.</a></p>
|
||||
<pre class="font-mono bg-inherit">{{ .license }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-logs" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content content card">
|
||||
<span class="heading">{{ .strings.logs }}<span class="modal-close">×</span></span>
|
||||
<pre class="monospace" id="log-area"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-modify-user" class="modal">
|
||||
<form class="modal-content card" id="form-modify-user" href="">
|
||||
<span class="heading">Modify Settings for <span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||
<p class="content">Apply settings from an existing profile, or source them directly from a user.</p>
|
||||
<div class="flex-row mb-1">
|
||||
<label class="flex-row-group mr-1">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-modify-user" href="">
|
||||
<span class="heading"><span id="header-modify-user"></span> <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.modifySettingsDescription }}</p>
|
||||
<div class="flex-row mb-4">
|
||||
<label class="flex-row-group mr-2">
|
||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-profile" checked>
|
||||
<span class="button ~neutral !high supra full-width center">Profile</span>
|
||||
<span class="button ~neutral @high supra full-width center">{{ .strings.profile }}</span>
|
||||
</label>
|
||||
<label class="flex-row-group ml-1">
|
||||
<label class="flex-row-group ml-2">
|
||||
<input type="radio" name="modify-user-source" class="unfocused" id="radio-use-user">
|
||||
<span class="button ~neutral !normal supra full-width center">User</span>
|
||||
<span class="button ~neutral @low supra full-width center">{{ .strings.user }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="select ~neutral !normal mb-1">
|
||||
<select id="modify-user-profiles">
|
||||
<option>Friends</option>
|
||||
<option>Family</option>
|
||||
<option>Default</option>
|
||||
</select>
|
||||
<div class="select ~neutral @low mb-4">
|
||||
<select id="modify-user-profiles"></select>
|
||||
</div>
|
||||
<div class="select ~neutral !normal mb-1 unfocused">
|
||||
<select id="modify-user-users">
|
||||
<option>Person</option>
|
||||
<option>Other person</option>
|
||||
</select>
|
||||
<div class="select ~neutral @low mb-4 unfocused">
|
||||
<select id="modify-user-users"></select>
|
||||
</div>
|
||||
<label class="switch mb-1">
|
||||
<label class="switch mb-4">
|
||||
<input type="checkbox" id="modify-user-homescreen" checked>
|
||||
<span>Apply homescreen layout</span>
|
||||
<span>{{ .strings.applyHomescreenLayout }}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Apply</span>
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.apply }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-delete-user" class="modal">
|
||||
<form class="modal-content card" id="form-delete-user" href="">
|
||||
<span class="heading">Delete <span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-half">
|
||||
<label class="switch mb-1">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-delete-user" href="">
|
||||
<span class="heading"><span id="header-delete-user"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-8">
|
||||
<label class="switch mb-4">
|
||||
<input type="checkbox" id="delete-user-notify" checked>
|
||||
<span>Send notification email</span>
|
||||
<span>{{ .strings.sendDeleteNotificationEmail }}</span>
|
||||
</label>
|
||||
<textarea id="textarea-delete-user" class="textarea full-width ~neutral !normal mb-1" placeholder="Your account has been deleted."></textarea>
|
||||
<textarea id="textarea-delete-user" class="textarea full-width ~neutral @low mb-4" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~critical !normal full-width center supra submit">Delete</span>
|
||||
<span class="button ~critical @low full-width center supra submit">{{ .strings.delete }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-restart" class="modal">
|
||||
<div class="modal-content card ~critical !low">
|
||||
<span class="heading">Restart needed <span class="modal-close">×</span></span>
|
||||
<p class="content pb-1">A restart is needed to apply some settings you changed. Do it now or later?</p>
|
||||
<div class="fr">
|
||||
<span class="button ~info !normal" id="settings-apply-no-restart">Apply, restart later</span>
|
||||
<span class="button ~critical !normal" id="settings-apply-restart">Apply & restart</span>
|
||||
<div id="modal-extend-expiry" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-extend-expiry" href="">
|
||||
<span class="heading"><span id="header-extend-expiry"></span> <span class="modal-close">×</span></span>
|
||||
<div class="content mt-8">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="extend-expiry-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="extend-expiry-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="extend-expiry-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="extend-expiry-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="extend-expiry-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch mb-4">
|
||||
<input type="checkbox" id="expiry-extend-enable" checked>
|
||||
<span>{{ .strings.sendDeleteNotificationEmail }}</span>
|
||||
</label>
|
||||
<textarea id="textarea-extend-enable" class="textarea full-width ~neutral @low mb-4" placeholder="{{ .strings.sendDeleteNotificationExample }}"></textarea>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~critical @low full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-refresh" class="modal">
|
||||
<div class="modal-content card ~neutral !normal">
|
||||
<span class="heading">Settings applied.</span>
|
||||
<p class="content">Refresh the page in a few seconds.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-ombi-defaults" class="modal">
|
||||
<form class="modal-content card" id="form-ombi-defaults" href="">
|
||||
<span class="heading">Ombi user defaults <span class="modal-close">×</span></span>
|
||||
<p class="content">Create an Ombi user and configure it, then select it here. It's settings/permissions will be stored and applied to new ombi users created by jfa-go.</p>
|
||||
<div class="select ~neutral !normal mb-1">
|
||||
<select>
|
||||
<option>Person</option>
|
||||
<option>Other person</option>
|
||||
</select>
|
||||
</div>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Submit</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-user-profiles" class="modal">
|
||||
<div class="modal-content wide card">
|
||||
<span class="heading">User profiles <span class="modal-close">×</span></span>
|
||||
<p class="support lg">Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.</p>
|
||||
<div id="modal-announce" class="modal">
|
||||
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-announce" href="">
|
||||
<span class="heading"><span id="header-announce"></span> <span class="modal-close">×</span></span>
|
||||
<div class="row">
|
||||
<div class="col card ~neutral @low">
|
||||
<div id="announce-details">
|
||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||
<div id="announce-variables">
|
||||
<span class="button ~urge @low mb-2 mt-4" id="announce-variables-username" style="margin-left: 0.25rem; margin-right: 0.25rem;"><span class="font-mono bg-inherit">{username}</span></span>
|
||||
</div>
|
||||
<label class="label supra" for="announce-subject"> {{ .strings.subject }}</label>
|
||||
<input type="text" id="announce-subject" class="input ~neutral @low mb-2 mt-4">
|
||||
<label class="label supra" for="textarea-announce">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-announce" class="textarea full-width ~neutral @low mt-4 font-mono"></textarea>
|
||||
<p class="support mt-4 mb-2">{{ .strings.markdownSupported }}</p>
|
||||
</div>
|
||||
<label class="label unfocused" id="announce-name"><p class="supra">{{ .strings.name }}</p>
|
||||
<input type="text" class="input ~neutral @low mb-2 mt-4">
|
||||
<p class="support">{{ .strings.templateEnterName }}</p>
|
||||
</label>
|
||||
<div class="row flex-expand">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low center supra submit">{{ .strings.send }}</span>
|
||||
</label>
|
||||
<span class="button ~info @low center supra" id="save-announce">{{ .strings.saveAsTemplate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col card ~neutral @low">
|
||||
<span class="subheading supra">{{ .strings.preview }}</span>
|
||||
<div class="mt-8" id="announce-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-customize" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading">{{ .strings.customizeMessages }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.customizeMessagesDescription }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Default</th>
|
||||
<th>From</th>
|
||||
<th>Libraries</th>
|
||||
<th><span class="button ~neutral !high" id="button-profile-create">Create</span></th>
|
||||
<th>{{ .strings.name }}</th>
|
||||
<th class="table-inline justify-center">{{ .strings.reset }}</th>
|
||||
<th>{{ .strings.edit }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-profiles">
|
||||
</tbody>
|
||||
<tbody id="customize-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-editor" class="modal">
|
||||
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-editor" href="">
|
||||
<span class="heading"><span id="header-editor"></span> <span class="modal-close">×</span></span>
|
||||
<div class="row">
|
||||
<div class="col card ~neutral @low">
|
||||
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span>
|
||||
<div id="editor-variables" class="mt-4"></div>
|
||||
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>
|
||||
<div id="editor-conditionals"></div>
|
||||
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
|
||||
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral @low mt-4 font-mono"></textarea>
|
||||
<p class="support mt-4 mb-2">{{ .strings.markdownSupported }}</p>
|
||||
<div class="flex-row">
|
||||
<label class="full-width ml-2">
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col card ~neutral @low">
|
||||
<span class="subheading supra">{{ .strings.preview }}</span>
|
||||
<div class="mt-8" id="editor-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-restart" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~critical @low">
|
||||
<span class="heading">{{ .strings.settingsRestartRequired }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.settingsRestartRequiredDescription }}</p>
|
||||
<div class="float-right">
|
||||
<span class="button ~info @low mb-2" id="settings-apply-no-restart">{{ .strings.settingsApplyRestartLater }}</span>
|
||||
<span class="button ~critical @low" id="settings-apply-restart">{{ .strings.settingsApplyRestartNow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-refresh" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<span class="heading">{{ .strings.settingsApplied }}</span>
|
||||
<p class="content">{{ .strings.settingsRefreshPage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-send-pwr" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<span class="heading">{{ .strings.sendPWR }}</span>
|
||||
<p class="content my-2" id="send-pwr-note"></p>
|
||||
<span class="button ~urge @low mt-2" id="send-pwr-link">{{ .strings.copy }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-ombi-profile" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-ombi-defaults" href="">
|
||||
<span class="heading">{{ .strings.ombiProfile }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.ombiUserDefaultsDescription }}</p>
|
||||
<div class="select ~neutral @low mb-4">
|
||||
<select></select>
|
||||
</div>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-user-profiles" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card">
|
||||
<span class="heading">{{ .strings.userProfiles }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.userProfilesDescription }}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ .strings.name }}</th>
|
||||
<th>{{ .strings.userProfilesIsDefault }}</th>
|
||||
{{ if .ombiEnabled }}
|
||||
<th>Ombi</th>
|
||||
{{ end }}
|
||||
<th>{{ .strings.from }}</th>
|
||||
<th>{{ .strings.userProfilesLibraries }}</th>
|
||||
<th><span class="button ~neutral @high" id="button-profile-create">{{ .strings.create }}</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-profiles"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-add-profile" class="modal">
|
||||
<form class="modal-content card" id="form-add-profile" href="">
|
||||
<span class="heading">Add profile <span class="modal-close">×</span></span>
|
||||
<p class="content">Create a Jellyfin user and configure it. Select it here, and when this profile is applied to an invite, new users will be created with its settings.</p>
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-add-profile" href="">
|
||||
<span class="heading">{{ .strings.addProfile }} <span class="modal-close">×</span></span>
|
||||
<p class="content my-4">{{ .strings.addProfileDescription }}</p>
|
||||
<label>
|
||||
<span class="supra">Profile Name </span>
|
||||
<input type="text" class="field input ~neutral !high mt-half mb-1" placeholder="Name" id="add-profile-name">
|
||||
<span class="supra">{{ .strings.addProfileNameOf }} </span>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.name }}" id="add-profile-name">
|
||||
<label>
|
||||
<span class="supra">User</span>
|
||||
<div class="select ~neutral !normal mt-half mb-1">
|
||||
<select id="add-profile-user">
|
||||
</select>
|
||||
<span class="supra">{{ .strings.user }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="add-profile-user"></select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="switch mb-1">
|
||||
<label class="switch mb-4">
|
||||
<input type="checkbox" id="add-profile-homescreen" checked>
|
||||
<span>Store homescreen layout</span>
|
||||
<span>{{ .strings.addProfileStoreHomescreenLayout }}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">Create</span>
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.create }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="modal-update" class="modal">
|
||||
<div class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card">
|
||||
<span class="heading">{{ .strings.updates }} <span class="modal-close">×</span></span>
|
||||
<p class="content">
|
||||
<h2 class="mt-2">
|
||||
<a id="update-version"></a> (<span class="font-mono bg-inherit" id="update-commit"></span>)
|
||||
</h2>
|
||||
<p class="content mt-2" id="update-description"></p>
|
||||
<p class="support mt-2" id="update-date"></p>
|
||||
<div class="content markdown-box mt-2" id="update-changelog"></div>
|
||||
</p>
|
||||
<span class="button ~info @low full-width center mt-2" id="update-download">{{ .strings.download }}</span>
|
||||
<span class="button ~urge @low full-width center mt-2" id="update-update">{{ .strings.update }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .telegramEnabled }}
|
||||
<div id="modal-telegram" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ .strings.linkTelegram }}</span>
|
||||
<p class="content mb-4">{{ .strings.sendPIN }}</p>
|
||||
<h1 class="ac" id="telegram-pin"></h1>
|
||||
<a class="subheading link-center" id="telegram-link" target="_blank">
|
||||
<span class="shield ~info mr-2">
|
||||
<span class="icon">
|
||||
<i class="ri-telegram-line"></i>
|
||||
</span>
|
||||
</span>
|
||||
@<span id="telegram-username">
|
||||
</a>
|
||||
<span class="button ~info @low full-width center mt-4" id="telegram-waiting">{{ .strings.success }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<div id="modal-discord" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4"><span id="discord-header"></span><span class="modal-close">×</span></span>
|
||||
<p class="content mb-4" id="discord-description"></p>
|
||||
<div class="row">
|
||||
<input type="search" class="col sm field ~neutral @low input" id="discord-search" placeholder="user#1234">
|
||||
</div>
|
||||
<table class="table"><tbody id="discord-list"></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div id="modal-matrix" class="modal">
|
||||
<form class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3" id="form-matrix" href="">
|
||||
<span class="heading">{{ .strings.linkMatrix }}</span>
|
||||
<p class="content my-4">{{ .strings.linkMatrixDescription }}</p>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.matrixHomeServer }}" id="matrix-homeserver">
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="matrix-user">
|
||||
<input type="password" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.password }}" id="matrix-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-12h">
|
||||
<span>{{ .strings.time12h }}</span>
|
||||
</label>
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-24h">
|
||||
<span>{{ .strings.time24h }}</span>
|
||||
</label>
|
||||
<div id="lang-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
</div>
|
||||
<div class="top-4 right-4 absolute">
|
||||
<a class="button ~info" href="/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
|
||||
</div>
|
||||
<div class="page-container">
|
||||
<div class="mb-1">
|
||||
<div class="mb-4">
|
||||
<header class="flex flex-wrap items-center justify-between">
|
||||
<div class="text-neutral-700">
|
||||
<span id="button-tab-invites" class="tab-button portal">Invites</span>
|
||||
<span id="button-tab-accounts" class="tab-button portal">Accounts</span>
|
||||
<span id="button-tab-settings" class="tab-button portal">Settings</span>
|
||||
<div>
|
||||
<span id="button-tab-invites" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.invites }}</span>
|
||||
<span id="button-tab-accounts" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.accounts }}</span>
|
||||
<span id="button-tab-settings" class="text-3xl button portal ~neutral dark:~d_neutral @low mr-2 mb-2 px-5">{{ .strings.settings }}</span>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<div class="text-neutral-700">
|
||||
<span class="button ~critical !normal mb-1 unfocused" id="logout-button">Logout</span>
|
||||
<span id="button-theme" class="button ~neutral !normal mb-1">Theme</span>
|
||||
<div class="mb-4">
|
||||
<div>
|
||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-invites">
|
||||
<div class="card ~neutral !low invites mb-1">
|
||||
<span class="heading">Invites</span>
|
||||
<div class="card @low invites dark:~d_neutral mb-4">
|
||||
<span class="heading">{{ .strings.invites }}</span>
|
||||
<div id="invites"></div>
|
||||
</div>
|
||||
<div class="card ~neutral !low">
|
||||
<span class="heading">Create</span>
|
||||
<div class="row" id="create-inv">
|
||||
<div class="card ~neutral !normal col">
|
||||
<label class="label supra" for="create-days">Days</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div class="card @low dark:~d_neutral">
|
||||
<span class="heading">{{ .strings.create }}</span>
|
||||
<div class="flex flex-col md:flex-row gap-3" id="create-inv">
|
||||
<div class="card ~neutral @low col">
|
||||
<div class="row mb-2">
|
||||
<label class="col mr-2">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||
<span class="button ~neutral @high supra full-width center">{{ .strings.inviteDuration }}</span>
|
||||
</label>
|
||||
<label class="col ml-2">
|
||||
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
|
||||
<span class="button ~neutral @low supra full-width center">{{ .strings.userExpiry }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="label supra" for="create-hours">Hours</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div id="inv-duration">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="create-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="create-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="create-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="label supra" for="create-minutes">Minutes</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<select id="create-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
<div id="user-expiry" class="unfocused">
|
||||
<p class="support mb-2">{{ .strings.userExpiryDescription }}</p>
|
||||
<div class="mb-2">
|
||||
<label for="create-user-expiry-enabled" class="button ~neutral @low">
|
||||
<input type="checkbox" id="create-user-expiry-enabled" aria-label="User duration enabled">
|
||||
<span class="ml-2">{{ .strings.enabled }} </span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-months">{{ .strings.inviteMonths }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="user-months">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-days">{{ .strings.inviteDays }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="user-days">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-hours">{{ .strings.inviteHours }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="user-hours">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="user-minutes">{{ .strings.inviteMinutes }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="user-minutes">
|
||||
<option>0</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label supra" for="create-label"> {{ .strings.label }}</label>
|
||||
<input type="text" id="create-label" class="input ~neutral @low mb-2 mt-4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col">
|
||||
<label class="label supra" for="create-uses">Number of uses</label>
|
||||
<div class="flex-expand mb-1 mt-half">
|
||||
<input type="number" min="0" id="create-uses" class="input ~neutral !normal mr-1" value=1>
|
||||
<label for="create-inf-uses" class="button ~neutral !normal">
|
||||
<span>∞</span>
|
||||
<div class="card ~neutral @low col">
|
||||
<label class="label supra" for="create-uses">{{ .strings.inviteNumberOfUses }}</label>
|
||||
<div class="flex-expand mb-2 mt-4">
|
||||
<input type="number" min="0" id="create-uses" class="input ~neutral @low mr-2" value=1>
|
||||
<label for="create-inf-uses" class="button ~neutral @low" title="Set uses to infinite">
|
||||
<span>∞</span>
|
||||
<input type="checkbox" class="unfocused" id="create-inf-uses" aria-label="Set uses to infinite">
|
||||
</label>
|
||||
</div>
|
||||
<p class="support unfocused" id="create-inf-uses-warning"><span class="badge ~critical">Warning</span> invites with infinite uses can be used abusively.</p>
|
||||
<label class="label supra">Profile</label>
|
||||
<div class="select ~neutral !normal mb-1 mt-half">
|
||||
<p class="support unfocused my-2" id="create-inf-uses-warning"><span class="badge ~critical">{{ .strings.warning }}</span> {{ .strings.inviteInfiniteUsesWarning }}</p>
|
||||
<label class="label supra">{{ .strings.profile }}</label>
|
||||
<div class="select ~neutral @low mb-2 mt-4">
|
||||
<select id="create-profile">
|
||||
</select>
|
||||
</div>
|
||||
<div id="create-send-to-container">
|
||||
<label class="label supra">Send to</label>
|
||||
<div class="flex-expand mb-1 mt-half">
|
||||
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
||||
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
||||
<div class="flex-expand mb-2 mt-4">
|
||||
{{ if .discordEnabled }}
|
||||
<input type="text" id="create-send-to" class="input ~neutral @low mr-2" placeholder="example@example.com | user#1234">
|
||||
<span id="create-send-to-search" class="button ~neutral @low mr-2">
|
||||
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
|
||||
</span>
|
||||
{{ else }}
|
||||
<input type="email" id="create-send-to" class="input ~neutral @low mr-2" placeholder="example@example.com">
|
||||
{{ end }}
|
||||
<label for="create-send-to-enabled" class="button ~neutral @low">
|
||||
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<span class="button ~urge !normal supra full-width center lg" id="create-submit">Create</span>
|
||||
<span class="button ~urge @low supra full-width center lg" id="create-submit">{{ .strings.create }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-accounts" class="unfocused">
|
||||
<div class="card ~neutral !low accounts mb-1">
|
||||
<span class="heading">Accounts</span>
|
||||
<div class="fr">
|
||||
<span class="button ~neutral !normal" id="accounts-add-user">Add User</span>
|
||||
<span class="button ~urge !normal" id="accounts-modify-user">Modify Settings</span>
|
||||
<span class="button ~critical !normal" id="accounts-delete-user">Delete User</span>
|
||||
<div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
|
||||
<div class="flex-expand align-middle">
|
||||
<span class="text-3xl font-bold mr-4">{{ .strings.accounts }}</span>
|
||||
<div id="accounts-filter-dropdown" class="dropdown z-10" tabindex="0">
|
||||
<span class="h-100 button ~neutral @low center" id="accounts-filter-button">{{ .strings.filters }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low mt-2" id="accounts-filter-list">
|
||||
<p class="supra pb-2">{{ .strings.filters }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
|
||||
<span class="button ~neutral @low center -ml-8" id="accounts-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||
</div>
|
||||
<div class="card ~neutral !normal accounts-header table-responsive mt-half">
|
||||
<table class="table">
|
||||
<div class="supra py-1 sm hidden" id="accounts-search-options-header">{{ .strings.searchOptions }}</div>
|
||||
<div class="row -mx-2">
|
||||
<button type="button" class="button ~neutral @low center m-2 hidden"><span id="accounts-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
||||
<span id="accounts-filter-area"></span>
|
||||
</div>
|
||||
<div class="supra py-1 sm">{{ .strings.actions }}</div>
|
||||
<div class="row -mx-2">
|
||||
<span class="col button ~neutral @low center max-w-[20%]" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||
<div id="accounts-announce-dropdown" class="col dropdown pb-0i max-w-[20%]" tabindex="0">
|
||||
<span class="w-100 button ~info @low center" id="accounts-announce">{{ .strings.announce }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<span class="supra sm">{{ .strings.templates }}</span>
|
||||
<div id="accounts-announce-templates"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="col button ~urge @low center max-w-[20%]" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||
<span class="col button ~warning @low center max-w-[20%]" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
||||
<div id="accounts-disable-enable-dropdown" class="col dropdown manual pb-0i max-w-[20%]" tabindex="0">
|
||||
<span class="w-100 button ~positive @low center" id="accounts-disable-enable">{{ .strings.disable }}</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<span class="button ~urge full-width accounts-announce-template-button" id="accounts-enable-expiry">{{ .strings.setExpiry }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="col button ~info @low center unfocused max-w-[20%]" id="accounts-send-pwr">{{ .strings.sendPWR }}</span>
|
||||
<span class="col button ~critical @low center max-w-[20%]" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||
</div>
|
||||
<div class="card @low accounts-header table-responsive mt-2">
|
||||
<table class="table text-base leading-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" value="" id="accounts-select-all"></th>
|
||||
<th>Username</th>
|
||||
<th>Email Address</th>
|
||||
<th>Last Active</th>
|
||||
<th class="table-inline my-2 grid gap-4 place-items-stretch accounts-header-username">{{ .strings.username }}</th>
|
||||
{{ if .jellyfinLogin }}
|
||||
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-access-jfa">{{ .strings.accessJFA }}</th>
|
||||
{{ end }}
|
||||
<th class="grid gap-4 place-items-stretch accounts-header-email">{{ .strings.emailAddress }}</th>
|
||||
{{ if .telegramEnabled }}
|
||||
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-telegram">Telegram</th>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-matrix">Matrix</th>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<th class="text-center-i grid gap-4 place-items-stretch accounts-header-discord">Discord</th>
|
||||
{{ end }}
|
||||
<th class="grid gap-4 place-items-stretch accounts-header-expiry">{{ .strings.expiry }}</th>
|
||||
<th class="grid gap-4 place-items-stretch accounts-header-last-active">{{ .strings.lastActiveTime }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="accounts-list"></tbody>
|
||||
@@ -275,22 +647,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-settings" class="unfocused">
|
||||
<div class="card ~neutral !low settings overflow">
|
||||
<span class="heading">Settings</span>
|
||||
<div class="fr">
|
||||
<span class="button ~neutral !normal unfocused" id="settings-save">Save</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="card ~neutral !normal col" id="settings-sidebar">
|
||||
<aside class="aside sm ~info mb-half">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-about"><span class="flex">About <i class="ri-information-line ml-half"></i></span></span>
|
||||
<span class="button ~neutral !low settings-section-button mb-half" id="setting-profiles"><span class="flex">User profiles <i class="ri-user-line ml-half"></i></span></span>
|
||||
<div class="card @low dark:~d_neutral settings overflow">
|
||||
<div class="flex-expand">
|
||||
<div class="flex-row">
|
||||
<span class="heading">{{ .strings.settings }}</span>
|
||||
<label for="settings-advanced-enabled" class="button ~neutral @low ml-2 my-2">
|
||||
<input type="checkbox" id="settings-advanced-enabled" aria-label="Advanced settings enabled">
|
||||
<span class="ml-2">{{ .strings.advancedSettings }} </span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="card ~neutral !normal col overflow" id="settings-panel"></div>
|
||||
<div>
|
||||
<span class="button ~info @low my-1" id="settings-logs">{{ .strings.logs }}</span>
|
||||
<span class="button ~neutral @low my-1" id="settings-restart">{{ .strings.settingsRestart }}</span>
|
||||
<span class="button ~urge @low unfocused my-1" id="settings-save">{{ .strings.settingsSave }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="card @low dark:~d_neutral col" id="settings-sidebar">
|
||||
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="settings-message">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info dark:~d_warning">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||
</div>
|
||||
<div class="card ~neutral @low col overflow" id="settings-panel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/admin.js" type="module"></script>
|
||||
<script src="{{ .urlBase }}/js/admin.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
45
html/crash.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link inline rel="stylesheet" type="text/css" href="bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>Crash report</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-container">
|
||||
<div class="card ~critical sectioned">
|
||||
<section class="section ~critical">
|
||||
<span class="heading">Crash report for jfa-go</span>
|
||||
{{ if .Err }}
|
||||
<div class="font-mono bg-inherit pre-line mt-4 mb-4">
|
||||
Error: {{ .Err }}
|
||||
</div>
|
||||
{{ end }}
|
||||
<a class="button ~critical mb-4" target="_blank" href="https://github.com/hrfee/jfa-go/issues/new/choose">Create an Issue</a>
|
||||
</section>
|
||||
<section class="section ~neutral @low">
|
||||
<div class="flex-expand">
|
||||
<span class="subheading">Full Log</span>
|
||||
<span class="button ~urge ml-4" id="copy-log">Copy</span>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<label class="col mr-4">
|
||||
<span class="button ~neutral @high supra full-width center" id="button-log-normal">Normal</span>
|
||||
</label>
|
||||
<label class="col mr-4">
|
||||
<span class="button ~neutral @low supra full-width center" id="button-log-sanitized">Sanitized</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="log-normal">
|
||||
<pre class="font-mono bg-inherit pre-line">{{ .Log }}</pre>
|
||||
</div>
|
||||
<div id="log-sanitized" class="unfocused">
|
||||
<p class="subheading">An attempt has been made to remove sensitive info, but make sure to check yourself.</p>
|
||||
<pre class="font-mono bg-inherit pre-line">{{ .SanitizedLog }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script inline src="crash.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
18
html/create-success.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.successHeader }} - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="heading mb-4">{{ .strings.successHeader }}</span>
|
||||
<p class="content my-4">{{ .successMessage }}</p>
|
||||
<a class="button ~urge @high full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
|
||||
</div>
|
||||
<i class="content">{{ .contactMessage }}</i>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,52 @@
|
||||
{{ define "form-base" }}
|
||||
<script>
|
||||
window.usernameEnabled = {{ .username }};
|
||||
window.validationStrings = JSON.parse({{ .lang.validationStrings }});
|
||||
window.invalidPassword = "{{ .lang.reEnterPasswordInvalid }}";
|
||||
window.validationStrings = JSON.parse({{ .validationStrings }});
|
||||
window.invalidPassword = "{{ .strings.reEnterPasswordInvalid }}";
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.code = "{{ .code }}";
|
||||
window.language = "{{ .langName }}";
|
||||
window.messages = JSON.parse({{ .notifications }});
|
||||
window.confirmation = {{ .confirmation }};
|
||||
window.userExpiryEnabled = {{ .userExpiry }};
|
||||
window.userExpiryMonths = {{ .userExpiryMonths }};
|
||||
window.userExpiryDays = {{ .userExpiryDays }};
|
||||
window.userExpiryHours = {{ .userExpiryHours }};
|
||||
window.userExpiryMinutes = {{ .userExpiryMinutes }};
|
||||
window.userExpiryMessage = {{ .userExpiryMessage }};
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramPIN = "{{ .telegramPIN }}";
|
||||
window.emailRequired = {{ .emailRequired }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.discordRequired = {{ .discordRequired }};
|
||||
window.discordPIN = "{{ .discordPIN }}";
|
||||
window.discordInviteLink = {{ .discordInviteLink }};
|
||||
window.discordServerName = "{{ .discordServerName }}";
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.matrixRequired = {{ .matrixRequired }};
|
||||
window.matrixUserID = "{{ .matrixUser }}";
|
||||
window.captcha = {{ .captcha }};
|
||||
window.reCAPTCHA = {{ .reCAPTCHA }};
|
||||
window.reCAPTCHASiteKey = "{{ .reCAPTCHASiteKey }}";
|
||||
window.userPageEnabled = {{ .userPageEnabled }};
|
||||
window.userPageAddress = "{{ .userPageAddress }}";
|
||||
</script>
|
||||
{{ if .passwordReset }}
|
||||
<script src="js/pwr.js" type="module"></script>
|
||||
{{ else }}
|
||||
<script src="js/form.js" type="module"></script>
|
||||
{{ if .reCAPTCHA }}
|
||||
<script>
|
||||
var reCAPTCHACallback = () => {
|
||||
const el = document.getElementsByClassName("g-recaptcha")[0];
|
||||
grecaptcha.render(el, {
|
||||
"sitekey": window.reCAPTCHASiteKey,
|
||||
"theme": document.documentElement.classList.contains("dark") ? "dark" : "light"
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="https://www.google.com/recaptcha/api.js?onload=reCAPTCHACallback&render=explicit" async defer></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
151
html/form.html
@@ -1,50 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .lang.pageTitle }}</title>
|
||||
{{ if .passwordReset }}
|
||||
<title>{{ .strings.passwordReset }}</title>
|
||||
{{ else }}
|
||||
<title>{{ .strings.pageTitle }}</title>
|
||||
{{ end }}
|
||||
<script>
|
||||
window.redirectToJellyfin = {{ .redirectToJellyfin }};
|
||||
</script>
|
||||
</head>
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
<div id="modal-success" class="modal">
|
||||
<div class="modal-content card">
|
||||
<span class="heading mb-1">{{ .lang.successHeader }}</span>
|
||||
<p class="content mb-1">{{ .successMessage }}</p>
|
||||
<a class="button ~urge !normal full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .lang.successContinueButton }}</a>
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ if .passwordReset }}{{ .strings.passwordReset }}{{ else }}{{ .strings.successHeader }}{{ end }}</span>
|
||||
<p class="content mb-4">{{ if .passwordReset }}{{ .strings.youCanLoginPassword }}{{ else }}{{ .successMessage }}{{ end }}</p>
|
||||
{{ if .userPageEnabled }}<p class="content mb-4" id="modal-success-user-page-area" my-account-term="{{ .strings.myAccount }}">{{ .strings.userPageSuccessMessage }}</p>{{ end }}
|
||||
<a class="button ~urge @low full-width center supra submit" href="{{ .jfLink }}" id="create-success-button">{{ .strings.continue }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-confirmation" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<span class="heading mb-4">{{ .strings.confirmationRequired }}</span>
|
||||
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "account-linking.html" . }}
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low" id="lang-list">
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div id="notification-box"></div>
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral !low">
|
||||
<div class="row baseline">
|
||||
<span class="col heading">{{ .lang.createAccountHeader }}</span>
|
||||
<span class="col subheading"> {{ .helpMessage }}</span>
|
||||
<div class="card dark:~d_neutral @low">
|
||||
<div class="flex flex-col md:flex-row gap-3 inline align-baseline">
|
||||
<span class="heading mr-5">
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.passwordReset }}
|
||||
{{ else }}
|
||||
{{ .strings.createAccountHeader }}
|
||||
{{ end }}
|
||||
</span>
|
||||
<span class="subheading">
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.enterYourPassword }}
|
||||
{{ else }}
|
||||
{{ .helpMessage }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form class="card ~neutral !normal" id="form-create" href="">
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="flex-1">
|
||||
{{ if .userExpiry }}
|
||||
<aside class="col aside sm ~warning" id="user-expiry-message"></aside>
|
||||
{{ end }}
|
||||
<form class="card dark:~d_neutral @low" id="form-create" href="">
|
||||
{{ if not .passwordReset }}
|
||||
<label class="label supra">
|
||||
{{ .lang.username }}
|
||||
<input type="text" class="input ~neutral !high mt-half mb-1" placeholder="{{ .lang.username }}" id="create-username" aria-label="{{ .lang.username }}">
|
||||
{{ .strings.username }}
|
||||
<input type="text" class="input ~neutral @high mt-2 mb-4" placeholder="{{ .strings.username }}" id="create-username" aria-label="{{ .strings.username }}">
|
||||
</label>
|
||||
|
||||
<label class="label supra" for="create-email">{{ .lang.emailAddress }}</label>
|
||||
<input type="email" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.emailAddress }}" id="create-email" aria-label="{{ .lang.emailAddress }}" value="{{ .email }}">
|
||||
|
||||
<label class="label supra" for="create-password">{{ .lang.password }}</label>
|
||||
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.password }}" id="create-password" aria-label="{{ .lang.password }}">
|
||||
|
||||
<label class="label supra" for="create-reenter-password">{{ .lang.reEnterPassword }}</label>
|
||||
<input type="password" class="input ~neutral 1high mt-half mb-1" placeholder="{{ .lang.password }}" id="create-reenter-password" aria-label="{{ .lang.reEnterPassword }}">
|
||||
|
||||
<label class="label supra" for="create-email">{{ .strings.emailAddress }}</label>
|
||||
<input type="email" class="input ~neutral @high mt-2 mb-4" placeholder="{{ .strings.emailAddress }}" id="create-email" aria-label="{{ .strings.emailAddress }}" value="{{ .email }}">
|
||||
{{ if .telegramEnabled }}
|
||||
<span class="button ~info @low full-width center mb-4" id="link-telegram">{{ .strings.linkTelegram }} {{ if .telegramRequired }}({{ .strings.required }}){{ end }}</span>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<span class="button ~info @low full-width center mb-4" id="link-discord">{{ .strings.linkDiscord }} {{ if .discordRequired }}({{ .strings.required }}){{ end }}</span>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<span class="button ~info @low full-width center mb-4" id="link-matrix">{{ .strings.linkMatrix }} {{ if .matrixRequired }}({{ .strings.required }}){{ end }}</span>
|
||||
{{ end }}
|
||||
{{ if or (.telegramEnabled) (or .discordEnabled .matrixEnabled) }}
|
||||
<div id="contact-via" class="unfocused">
|
||||
<label class="row switch pb-4 unfocused">
|
||||
<input type="checkbox" name="contact-via" value="email" id="contact-via-email" class="mr-2"><span>Contact through Email</span>
|
||||
</label>
|
||||
{{ if .telegramEnabled }}
|
||||
<label class="row switch pb-4 unfocused">
|
||||
<input type="checkbox" name="contact-via" value="telegram" id="contact-via-telegram" class="mr-2"><span>Contact through Telegram</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
{{ if .discordEnabled }}
|
||||
<label class="row switch pb-4 unfocused">
|
||||
<input type="checkbox" name="contact-via" value="discord" id="contact-via-discord" class="mr-2"><span>Contact through Discord</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
{{ if .matrixEnabled }}
|
||||
<label class="row switch pb-4 unfocused">
|
||||
<input type="checkbox" name="contact-via" value="matrix" id="contact-via-matrix" class="mr-2"><span>Contact through Matrix</span>
|
||||
</label>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<label class="label supra" for="create-password">{{ .strings.password }}</label>
|
||||
<input type="password" class="input ~neutral @high mt-2 mb-4" placeholder="{{ .strings.password }}" id="create-password" aria-label="{{ .strings.password }}">
|
||||
|
||||
<label class="label supra" for="create-reenter-password">{{ .strings.reEnterPassword }}</label>
|
||||
<input type="password" class="input ~neutral @high mt-2 mb-4" placeholder="{{ .strings.password }}" id="create-reenter-password" aria-label="{{ .strings.reEnterPassword }}">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge !normal full-width center supra submit">{{ .lang.createAccountButton }}</span>
|
||||
<span class="button ~urge @low full-width center supra submit">
|
||||
{{ if .passwordReset }}
|
||||
{{ .strings.reset }}
|
||||
{{ else }}
|
||||
{{ .strings.createAccountButton }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card ~neutral !normal">
|
||||
<span class="label supra" for="inv-uses">{{ .lang.passwordRequirementsHeader }}</span>
|
||||
<div class="flex-initial">
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="label supra">{{ .strings.passwordRequirementsHeader }}</span>
|
||||
<ul>
|
||||
{{ range $key, $value := .requirements }}
|
||||
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
||||
@@ -53,17 +133,22 @@
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if .captcha }}
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="label supra mb-2">CAPTCHA {{ if not .reCAPTCHA }}<span id="captcha-regen" title="{{ .strings.refresh }}" class="badge lg @low ~info ml-2 float-right"><i class="ri-refresh-line"></i></span><span id="captcha-success" class="badge lg @low ~critical ml-2 float-right"><i class="ri-close-line"></i></span>{{ end }}</span>
|
||||
<div id="captcha-img" class="mt-2 mb-2 {{ if .reCAPTCHA }}g-recaptcha{{ end }}"></div>
|
||||
{{ if not .reCAPTCHA }}
|
||||
<input class="field ~neutral @low" id="captcha-input" class="mt-2" placeholder="CAPTCHA">
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .contactMessage }}
|
||||
<aside class="col aside sm ~info">{{ .contactMessage }}</aside>
|
||||
<aside class="col aside sm ~info mt-4">{{ .contactMessage }}</aside>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.validationStrings = {{ .lang.validationStrings }};
|
||||
</script>
|
||||
{{ template "form-base" . }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="Description" content="jfa-go, a better way to manage Jellyfin users.">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .urlBase }}/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ .urlBase }}/favicon-32x32.png">
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/base.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>Invalid Code - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
<div class="page-container">
|
||||
<h1 class="heading">Invalid invite code.</h1>
|
||||
<p class="content">The code above was either incorrect, or has expired.</p>
|
||||
<p class="content">
|
||||
{{ .contactMessage }}
|
||||
</p>
|
||||
<div class="card">
|
||||
<h1 class="text-3xl font-semibold">Invalid invite code.</h1>
|
||||
<p class="content">The code above was either incorrect, or has expired.</p>
|
||||
<p class="content">
|
||||
{{ .contactMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
25
html/login-modal.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<div id="modal-login" class="modal">
|
||||
<div class="my-[10%] row items-stretch relative mx-auto w-[40%] lg:w-[60%]">
|
||||
{{ if index . "LoginMessageEnabled" }}
|
||||
{{ if .LoginMessageEnabled }}
|
||||
<div class="card mx-2 flex-initial w-[100%] xl:w-[35%] mb-4 xl:mb-0 dark:~d_neutral @low content">
|
||||
{{ .LoginMessageContent }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<form class="card mx-2 flex-auto form-login w-[100%] xl:w-[55%] mb-0" href="">
|
||||
<span class="heading">{{ .strings.login }}</span>
|
||||
<input type="text" class="field input ~neutral @high mt-4 mb-2" placeholder="{{ .strings.username }}" id="login-user">
|
||||
<input type="password" class="field input ~neutral @high mb-4" placeholder="{{ .strings.password }}" id="login-password">
|
||||
<label>
|
||||
<input type="submit" class="unfocused">
|
||||
<span class="button ~urge @low full-width center supra submit">{{ .strings.login }}</span>
|
||||
{{ if index . "pwrEnabled" }}
|
||||
{{ if .pwrEnabled }}
|
||||
<span class="button ~info @low full-width center supra submit my-2" id="modal-login-pwr">{{ .strings.resetPassword }}</span>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
45
html/password-reset.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="{{ .cssClass }}">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.passwordReset }} - jfa-go</title>
|
||||
</head>
|
||||
<body class="section">
|
||||
{{ if .success }}
|
||||
<div id="notification-box">
|
||||
<span id="copy-notification" class="unfocused">{{ .strings.copied }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="page-container">
|
||||
<div class="card ~neutral @low mb-4">
|
||||
<span class="heading mb-4">
|
||||
{{ if .success }}
|
||||
{{ .strings.passwordReset }}
|
||||
{{ else }}
|
||||
{{ .strings.resetFailed }}
|
||||
{{ end }}
|
||||
</span>
|
||||
<p class="content mb-4">
|
||||
{{ if .success }}
|
||||
{{ if .ombiEnabled }}
|
||||
{{ .strings.youCanLoginOmbi }}
|
||||
{{ else }}
|
||||
{{ .strings.youCanLogin }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
{{ .strings.tryAgain }}
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ if .success }}
|
||||
<aside class="aside ~warning">
|
||||
{{ .strings.changeYourPassword }}
|
||||
</aside>
|
||||
<span class="button ~urge @low w-100 text-center text-xl p-1 mt-4" id="pin" title="{{ .strings.copy }}">{{ .pin }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<i class="content">{{ .contactMessage }}</i>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/pwr-pin.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
844
html/setup.html
@@ -1,374 +1,502 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-alpha3/dist/js/bootstrap.min.js" integrity="sha384-t6I8D5dJmMXjCsRLhSzCltuhNZg6P10kE0m0nAncLUjH6GeYLhRU1zfLoW3QNQDF" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
.card-body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10%;
|
||||
}
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: none;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
.slider > div {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
.slide {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
margin: 10%;
|
||||
}
|
||||
</style>
|
||||
<title>Setup - jfa-go</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .lang.Strings.pageTitle }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pageContainer">
|
||||
<div class="container">
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
<div id="notification-box"></div>
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low" id="lang-list">
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="page-container" id="page-container">
|
||||
<div class="card ~neutral @low mb-2">
|
||||
<div class="row">
|
||||
<div class="col-sm"></div>
|
||||
<div id="setupCarousel" class="col-md-auto slider">
|
||||
<div class="slide card text-center" id="page-1">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Welcome!</h5>
|
||||
<p class="card-text">
|
||||
You'll need to do a few things to start using jfa-go. Click below to get started, or quit and edit the config file manually.
|
||||
</p>
|
||||
<a class="btn btn-primary nextButton" href="#page-2">Get Started</a>
|
||||
<img class="banner header" src="banner.svg" alt="jfa-go" />
|
||||
</div>
|
||||
<div class="row col flex center">
|
||||
<span class="heading welcome">{{ .lang.StartPage.welcome }}</span>
|
||||
</div>
|
||||
<div class="row col flex center">
|
||||
<p class="content my-2">{{ .lang.StartPage.pressStart }}</p>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="support">{{ .lang.StartPage.httpsNotice }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.StartPage.start }}</span>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.Language.title }}</span>
|
||||
<p class="content my-2" id="language-description"></p>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Language.defaultAdminLang }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="ui-language-admin">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Language.defaultFormLang }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="ui-language-form">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Language.defaultEmailLang }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="email-language">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.General.title }}</span>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.General.listenAddress }}</span>
|
||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="ui-host" value="0.0.0.0">
|
||||
</label>
|
||||
<label class="row switch">
|
||||
<input type="checkbox" class="mr-2" id="advanced-tls"><span>{{ .lang.General.useHTTPS }}</span>
|
||||
</label>
|
||||
<p class="support mb-2 mt-1">{{ .lang.General.useHTTPSNotice }}</p>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.General.pathToCertificate }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-tls_cert">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.General.pathToKeyFile }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="advanced-tls_key">
|
||||
</label>
|
||||
<span class="heading">{{ .lang.Updates.title }}</span>
|
||||
<p class="content my-2" id="updates-description"></p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="updates-enabled" checked><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>{{ .lang.Updates.updateChannel }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="updates-channel">
|
||||
<option value="stable">{{ .lang.Updates.stable }}</option>
|
||||
<option value="unstable">{{ .lang.Updates.unstable }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<small>Note: Make sure you are accessing this page through HTTPS, or on a private network.</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.port }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="ui-port" value="8056">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.General.httpsPort }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="advanced-tls_port" value="8057">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.General.urlBase }} ({{ .lang.Strings.optional }})</span>
|
||||
<input type="url" class="input ~neutral @low mt-4" id="ui-url_base">
|
||||
<p class="support mb-2 mt-1">{{ .lang.General.urlBaseNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>{{ .lang.Strings.theme }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="ui-theme">
|
||||
<option value="Jellyfin (Dark)">{{ .lang.General.darkTheme }}</option>
|
||||
<option value="Default (Light)">{{ .lang.General.lightTheme }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.Login.title }}</span>
|
||||
<p class="content my-2">{{ .lang.Login.description }}</p>
|
||||
<div class="pl-4">
|
||||
<label class="row switch pb-4">
|
||||
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="true" checked><span>{{ .lang.Login.authorizeWithJellyfin }}</span>
|
||||
</label>
|
||||
<label class="row switch pl-4 pb-4">
|
||||
<input type="checkbox" class="mr-2" id="ui-admin_only" checked><span>{{ .lang.Login.adminOnly }}</span>
|
||||
</label>
|
||||
<label class="row switch pl-4 pb-2">
|
||||
<input type="checkbox" class="mr-2" id="ui-allow_all"><span>{{ .lang.Login.allowAll }}</span>
|
||||
</label>
|
||||
<p class="support pb-4 pl-4 mt-1" id="description-ui-allow_all">{{ .lang.Login.allowAllDescription }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="login-manual">
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.username }}</span>
|
||||
<input type="text" id="ui-username" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.username }}">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>{{ .lang.Strings.password }}</span>
|
||||
<input type="password" id="ui-password" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.password }}">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>{{ .lang.Strings.emailAddress }} ({{ .lang.Strings.optional }})</span>
|
||||
<input type="email" id="ui-email" class="input ~neutral @low mt-4" placeholder="email@address">
|
||||
<span class="support mb-2 mt-1">{{ .lang.Login.emailNotice }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.JellyfinEmby.title }}</span>
|
||||
<p class="content my-2">{{ .lang.JellyfinEmby.description }}</p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label">
|
||||
<span>{{ .lang.Strings.serverType }}</span>
|
||||
<div class="select ~neutral @low mt-4">
|
||||
<select id="jellyfin-type">
|
||||
<option value="jellyfin">Jellyfin</option>
|
||||
<option value="emby">Emby</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="support mb-2 mt-1">{{ .lang.JellyfinEmby.embyNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.JellyfinEmby.replaceJellyfin }} ({{ .lang.Strings.optional }})</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="jellyfin-substitute_jellyfin_strings">
|
||||
<p class="support mb-2 mt-1">{{ .lang.JellyfinEmby.replaceJellyfinNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.username }}</span>
|
||||
<input type="text" id="jellyfin-username" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.username }}">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span>{{ .lang.Strings.password }}</span>
|
||||
<input type="password" id="jellyfin-password" class="input ~neutral @low mt-4 mb-2" placeholder="{{ .lang.Strings.password }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.internal }})</span>
|
||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="jellyfin-server" placeholder="http://jellyf.in:80">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.serverAddress }} ({{ .lang.JellyfinEmby.external }})</span>
|
||||
<input type="url" class="input ~neutral @low mt-4" id="jellyfin-public_server" placeholder="https://jellyf.in">
|
||||
<p class="support mb-2 mt-1">{{ .lang.JellyfinEmby.addressExternalNotice }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low" id="jellyfin-test-connection">{{ .lang.JellyfinEmby.testConnection }}</span>
|
||||
<span class="button ~urge @low next" disabled>{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.Ombi.title }}</span>
|
||||
<p class="content my-2">{{ .lang.Ombi.description }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="ombi-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.serverAddress }}</span>
|
||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="ombi-server" placeholder="ombi.jellyf.in">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.apiKey }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="ombi-api_key">
|
||||
<p class="support mb-2 mt-1">{{ .lang.Ombi.apiKeyNotice }}</p>
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.Messages.title }}</span>
|
||||
<p class="content my-2" id="messages-description"></p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="messages-enabled" checked><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Email.dateFormat }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="email-date_format" value="%d/%m/%y">
|
||||
<p class="support mb-2 mt-1" id="email-dateformat-notice"></p>
|
||||
</label>
|
||||
<div>
|
||||
<label class="row switch pb-4">
|
||||
<input type="radio" class="mr-2" name="email-24h" value="true" checked><span>{{ .lang.Strings.time24h }}</span>
|
||||
</label>
|
||||
<label class="row switch pb-4">
|
||||
<input type="radio" class="mr-2" name="email-24h" value="false"><span>{{ .lang.Strings.time12h }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="email-sect">
|
||||
<span class="heading">{{ .lang.Email.title }}</span>
|
||||
<p class="content my-2" id="email-description"></p>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label class="label">
|
||||
<span>{{ .lang.Email.method }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="email-method">
|
||||
<option value="">{{ .lang.Strings.disabled }}</option>
|
||||
<option value="smtp">SMTP</option>
|
||||
<option value="mailgun">Mailgun</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="row switch">
|
||||
<input type="checkbox" class="mr-2" id="email-no_username"><span>{{ .lang.Email.useEmailAsUsername }}</span>
|
||||
<p class="support mb-2 mt-1">{{ .lang.Email.useEmailAsUsernameNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Email.fromAddress }}</span>
|
||||
<input type="email" class="input ~neutral @low mt-4 mb-2" id="email-address" placeholder="mail@jellyf.in">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Email.senderName }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="email-from" value="Jellyfin">
|
||||
</label>
|
||||
</div>
|
||||
<div class="slide card" id="page-2">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Login</h5>
|
||||
<p class="card-text">
|
||||
To access the admin page, you'll need to login. Choose how below.
|
||||
<ul>
|
||||
<li><b>Authorize through Jellyfin: </b>Checks credentials with Jellyfin, allowing you to share login details and grant multiple users access.</li>
|
||||
<li><b>Username & Password: </b>Set your own username and password manually.</li>
|
||||
</ul>
|
||||
<div class="form-check" id="jfAuthFormGroup">
|
||||
<input class="form-check-input" type="radio" name="auth" id="jfAuthRadio" value="jfAuth" checked>
|
||||
<label class="form-check-label" for="jfAuthRadio">
|
||||
Authorize through Jellyfin
|
||||
</label>
|
||||
</div>
|
||||
<div id="adminOnlyArea">
|
||||
<div class="form-check" style="margin-left: 1rem;">
|
||||
<input type="checkbox" class="form-check-input" id="jfAuthAdminOnly" checked>
|
||||
<label for="jfAuthAdminOnly" class="form-check-label">Allow admin users only</label>
|
||||
<div class="col">
|
||||
<div id="email-smtp">
|
||||
<p class="text-2xl font-semibold mb-2">SMTP</p>
|
||||
<label class="label">
|
||||
<span>{{ .lang.Email.encryption }}</span>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="smtp-encryption">
|
||||
<option value="starttls">STARTTLS ({{ .lang.Strings.port }} 587)</option>
|
||||
<option value="ssl_tls">SSL/TLS ({{ .lang.Strings.port }} 465)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="auth" id="manualAuthRadio" value="manualAuth">
|
||||
<label class="form-check-label" for="manualAuthRadio">
|
||||
Manual username & password
|
||||
</label>
|
||||
</div>
|
||||
<div id="manualAuthArea">
|
||||
<div class="form-group">
|
||||
<label for="manualAuthUsername">Username</label>
|
||||
<input type="text" class="form-control" id="manualAuthUsername" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manualAuthPassword">Password</label>
|
||||
<input type="password" class="form-control" id="manualAuthPassword" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="manualAuthEmail">Email (Optional)</label>
|
||||
<input type="email" class="form-control" id="manualAuthEmail" placeholder="example@example.com">
|
||||
<small class="form-text text-muted">Your email address is only required if you want to recieve activity notifications.</small>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-1">Back</a>
|
||||
<a class="btn btn-primary nextButton" href="#page-3">Next</a>
|
||||
</div>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.serverAddress }}</span>
|
||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="smtp-server" placeholder="smtp.jellyf.in">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.port }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="smtp-port" placeholder="587">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.username }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="smtp-username">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.password }}</span>
|
||||
<input type="password" class="input ~neutral @low mt-4 mb-2" id="smtp-password">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Jellyfin</h5>
|
||||
<p class="card-text">
|
||||
jfa-go needs admin access so that it can create users, as this is currently not permitted via API tokens.
|
||||
You should create a separate account for it, checking 'Allow this user to manage the server'. You can disable everything else. Once done, enter the credentials here.
|
||||
<div class="form-group">
|
||||
<label for="jfHost">Host (For internal use)</label>
|
||||
<input type="url" class="form-control" id="jfHost" placeholder="http://jellyf.in:443" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jfPublicHost">Public Host (For access by users)</label>
|
||||
<input type="url" class="form-control" id="jfPublicHost" placeholder="Leave blank to use the above address.">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jfUser">Username</label>
|
||||
<input type="text" class="form-control" id="jfUser" placeholder="Username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="jfPassword">Password</label>
|
||||
<input type="password" class="form-control" id="jfPassword" placeholder="Password" required>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<button class="btn btn-secondary" id="jfTestButton">Test</button>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-2">Back</a>
|
||||
<a class="btn btn-primary nextButton disabled" id="jfNextButton" aria-disabled="true" href="#page-4">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Email</h5>
|
||||
<p class="card-text">jfa-go is capable of sending a PIN code when a user tries to reset their password on Jellyfin. One can also choose to send an invite code directly to an email address. This can be done through SMTP or through <a href="https://www.mailgun.com/">Mailgun's</a> API.
|
||||
<div class="form-group">
|
||||
<div class="form-check" id="emailDisabled">
|
||||
<input class="form-check-input" type="radio" name="email" id="emailDisabledRadio" value="emailDisabled">
|
||||
<label class="form-check-label" for="emailDisabledRadio">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check" id="emailSMTP">
|
||||
<input class="form-check-input" type="radio" name="email" id="emailSMTPRadio" value="emailSMTP" checked>
|
||||
<label class="form-check-label" for="emailSMTPRadio">
|
||||
SMTP
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="email" id="emailMailgunRadio" value="emailMailgun">
|
||||
<label class="form-check-label" for="emailMailgunRadio">
|
||||
Mailgun API
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailSMTPArea">
|
||||
<div class="form-group form-check form-switch">
|
||||
<input type="checkbox" class="form-check-input" id="emailSSL_TLS" checked>
|
||||
<label for="emailSSL_TLS" class="form-check-label" id="emailSSL_TLSLabel">Use SSL/TLS</label>
|
||||
<small class="form-text text-muted">Note: SSL/TLS usually uses port 465, whereas STARTTLS usually uses 587.</small>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" id="emailSMTPServer" placeholder="SMTP Server Address">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="number" class="form-control" id="emailSMTPPort" placeholder="Port">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<div class="col">
|
||||
<input type="email" class="form-control" id="emailSMTPAddress" placeholder="jellyfin@jellyf.in">
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="password" class="form-control" id="emailSMTPPassword" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailMailgunArea">
|
||||
<div class="form-group">
|
||||
<input type="url" class="form-control" id="emailMailgunURL" placeholder="API URL">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="emailMailgunKey" placeholder="API Key">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control" id="emailMailgunAddress" placeholder="jellyfin@jellyf.in">
|
||||
</div>
|
||||
</div>
|
||||
<div id="emailCommonArea">
|
||||
<h5 class="card-title">Notifications</h5>
|
||||
<p class="card-text">Enabling notifications will allow you to choose (per-invite) to recieve emails when an invite expires, or when a new user is created. If you chose to use Manual auth instead of Jellyfin auth previously, make sure you provided an email address.</p>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="notificationsEnabled">
|
||||
<label for="notificationsEnabled" class="form-check-label">Enabled</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-3">Back</a>
|
||||
<a class="btn btn-primary nextButton" id="emailNextButton" href="#page-5">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-5">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Email</h5>
|
||||
<p class="card-text">Just a few more things to get your emails looking great.
|
||||
<div class="form-group">
|
||||
<label for="emailSender">Sender: The name shown when a user receives an email.</label>
|
||||
<input type="text" class="form-control" id="emailSender" value="Jellyfin">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="emailDateFormat">Date Format: Follows <a target="_blank" href="https://strftime.org/">strftime</a> format.</label>
|
||||
<input type="text" class="form-control" id="emailDateFormat" value="%d/%m/%y">
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input class="form-check-input" type="radio" name="time" id="email24hTimeRadio" value="email24hTime" checked>
|
||||
<label class="form-check-label" for="email24hTimeRadio">24h time</label>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input class="form-check-input" type="radio" name="time" id="email12hTimeRadio" value="email12hTime">
|
||||
<label class="form-check-label" for="email12hTimeRadio">12h time</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="emailMessage">Message: Short message displayed at the bottom of emails.</label>
|
||||
<input type="text" class="form-control" id="emailMessage" value="Need help? Contact me.">
|
||||
</div>
|
||||
</p>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-4">Back</a>
|
||||
<a class="btn btn-primary nextButton" href="#page-6">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-6">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Password Resets</h5>
|
||||
<p class="card-text">
|
||||
When a user tries to reset their password in jellyfin, it informs them that a file has been created, named "passwordreset*.json" where * is a number. jfa-go will then read this file, and send the PIN to the user's email. Try it now, and put the folder that it informs you it put the file in below. Also, if enter a custom email subject if you don't like the default one.
|
||||
</p>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="pwrEnabled" value="enabled">
|
||||
<label class="form-check-label" for="pwrEnabled">Enabled</label>
|
||||
</div>
|
||||
<div id="pwrArea">
|
||||
<div class="form-group">
|
||||
<label for="pwrJfPath">Path to Jellyfin</label>
|
||||
<input type="text" class="form-control" id="pwrJfPath" placeholder="Folder">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pwrSubject">Email Subject</label>
|
||||
<input type="text" class="form-control" id="pwrSubject" value="Password Reset - Jellyfin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-5">Back</a>
|
||||
<a class="btn btn-primary nextButton" href="#page-7">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-7">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Invite Emails</h5>
|
||||
<p class="card-text">
|
||||
Allows you to send an invite code directly to a specified email address.
|
||||
Since you'll most likely being running this behind a reverse proxy, the program has no way of knowing the address it will be accessed from. This is needed for sending emails with links. Write your URL Base with the protocol and append '/invite', e.g:
|
||||
<ul>
|
||||
<li>On the local network, you might use <a href="#">http://localhost:8056/invite</a></li>
|
||||
<li>Exposed to the internet, you might use <a href="#">https://accounts.jellyf.in/invite</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="invEnabled" value="enabled" checked>
|
||||
<label class="form-check-label" for="invEnabled">Enabled</label>
|
||||
</div>
|
||||
<div id="invArea">
|
||||
<div class="form-group">
|
||||
<label for="invURLBase">URL Base</label>
|
||||
<input type="url" class="form-control" id="invURLBase" placeholder="https://accounts.jellyf.in/invite">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="invSubject">Subject</label>
|
||||
<input type="text" class="form-control" id="invSubject" value="Invite - Jellyfin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-6">Back</a>
|
||||
<a class="btn btn-primary nextButton" href="#page-8">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-8">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Password Validation</h5>
|
||||
<p class="card-text">
|
||||
Enabling this will display a set of password requirements on the create account page, such as minimum length, uppercase characters, special characters, etc.
|
||||
</p>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="valEnabled" value="enabled">
|
||||
<label class="form-check-label" for="valEnabled">Enabled</label>
|
||||
</div>
|
||||
<div id="valArea">
|
||||
<div class="form-group">
|
||||
<label for="valLength">Minimum Length</label>
|
||||
<input type="number" class="form-control" id="valLength" value="8">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="valUpper">Minimum number of uppercase characters</label>
|
||||
<input type="number" class="form-control" id="valUpper" value="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="valLower">Minimum number of lowercase characters</label>
|
||||
<input type="number" class="form-control" id="valLower" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="valNumber">Minimum number of numbers</label>
|
||||
<input type="number" class="form-control" id="valNumber" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="valSpecial">Minimum number of special characters</label>
|
||||
<input type="number" class="form-control" id="valSpecial" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" id="valBackButton" href="#page-7">Back</a>
|
||||
<a class="btn btn-primary nextButton" href="#page-9">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-9">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Help Messages</h5>
|
||||
<p class="card-text">
|
||||
Just a few little messages that will display in various places. Leave these alone if you want.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="msgContact">Contact message: Displays at bottom of all pages (except admin).</label>
|
||||
<input id="msgContact" type="text" class="form-control" value="Need help? Contact me.">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="msgHelp">Help message: Displays when a user is creating an account.</label>
|
||||
<input id="msgHelp" type="text" class="form-control" value="Enter your details to create an account.">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="msgSuccess">Success message: Displays when a user successfully creates an account, just above a button taking the user to Jellyfin.</label>
|
||||
<input id="msgSuccess" type="text" class="form-control" value="Your account has been created. Click below to continue to Jellyfin.">
|
||||
</div>
|
||||
<div class="btn-group float-right" role="group" aria-label="Back/Next buttons">
|
||||
<a class="btn btn-secondary backButton" href="#page-8">Back</a>
|
||||
<a class="btn btn-primary nextButton" href="#page-10">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide card" id="page-10">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">Finished!</h5>
|
||||
<p class="card-text">
|
||||
Press the button below to submit your settings. The program will restart. Once it's done, refresh this page.
|
||||
</p>
|
||||
<button id="submitButton" class="btn btn-primary">Submit</button>
|
||||
<div id="email-mailgun">
|
||||
<p class="text-2xl font-semibold mb-2">Mailgun</p>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Email.mailgunApiURL }}</span>
|
||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="mailgun-api_url" placeholder="https://api.eu.mailgun.net/v3/mail.jellyf.in/messages">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.apiKey }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="mailgun-api_key">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm"></div>
|
||||
</div>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused related-to-email">
|
||||
<span class="heading">{{ .lang.Notifications.title }}</span>
|
||||
<p class="content my-2">{{ .lang.Notifications.description }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="notifications-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<span class="heading">{{ .lang.WelcomeEmails.title }}</span>
|
||||
<p class="content my-2">{{ .lang.WelcomeEmails.description }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="welcome_email-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="welcome_email-subject" placeholder="{{ .emailLang.WelcomeEmail.title }}">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused related-to-email">
|
||||
<span class="heading">{{ .lang.InviteEmails.title }}</span>
|
||||
<p class="content my-2">{{ .lang.InviteEmails.description }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="invite_emails-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.URL }}</span>
|
||||
<input type="url" class="input ~neutral @low mt-4 mb-2" id="invite_emails-url_base" placeholder="https://accounts.jellyf.in/invite">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="invite_emails-subject" placeholder="{{ .emailLang.InviteEmail.title }}">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="password-resets" class="card ~neutral @low mb-2 unfocused related-to-email">
|
||||
<span class="heading">{{ .lang.PasswordResets.title }}</span>
|
||||
<p class="content my-2">{{ .lang.PasswordResets.description }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="password_resets-enabled"><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.PasswordResets.pathToJellyfin }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="password_resets-watch_directory" placeholder="/config/jellyfin">
|
||||
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.pathToJellyfinNotice }}</p>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" class="mr-2" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span>
|
||||
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.resetLinksNotice }}</p>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" class="mr-2" id="password_resets-set_password"><span>{{ .lang.PasswordResets.setPassword }}</span>
|
||||
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.setPasswordNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<p class="mt-4">{{ .lang.PasswordResets.resetLinksLanguage }}</p>
|
||||
<div class="select ~neutral @low mt-4 mb-2">
|
||||
<select id="password_resets-language">
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label class="row label">
|
||||
<span class="mt-4">{{ .lang.Strings.emailSubject }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4 mb-2" id="password_resets-subject" placeholder="{{ .emailLang.PasswordReset.title }}">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.PasswordValidation.title }}</span>
|
||||
<p class="content my-2">{{ .lang.PasswordValidation.description }}</p>
|
||||
<label class="row switch pb-4">
|
||||
<input type="checkbox" class="mr-2" id="password_validation-enabled" checked><span>{{ .lang.Strings.enabled }}</span>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.PasswordValidation.length }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-min_length" value="8">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.PasswordValidation.uppercase }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-upper" value="1">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.PasswordValidation.lowercase }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-lower" value="0">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.PasswordValidation.numbers }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-number" value="0">
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.PasswordValidation.special }}</span>
|
||||
<input type="number" class="input ~neutral @low mt-4 mb-2" id="password_validation-special" value="0">
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<span class="heading">{{ .lang.HelpMessages.title }}</span>
|
||||
<p class="content my-2">{{ .lang.HelpMessages.description }}</p>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.HelpMessages.contactMessage }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="ui-contact_message">
|
||||
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.contactMessageNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.HelpMessages.helpMessage }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="ui-help_message">
|
||||
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.helpMessageNotice }}</p>
|
||||
</label>
|
||||
<label class="label">
|
||||
<span class="mt-4">{{ .lang.HelpMessages.successMessage }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="ui-success_message">
|
||||
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.successMessageNotice }}</p>
|
||||
</label>
|
||||
<label class="label related-to-email">
|
||||
<span class="mt-4">{{ .lang.HelpMessages.emailMessage }}</span>
|
||||
<input type="text" class="input ~neutral @low mt-4" id="email-message">
|
||||
<p class="support mb-2 mt-1">{{ .lang.HelpMessages.emailMessageNotice }}</p>
|
||||
</label>
|
||||
<section class="section ~neutral banner footer flex-expand middle">
|
||||
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
|
||||
<div>
|
||||
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="card ~neutral @low mb-2 unfocused">
|
||||
<div class="row col flex center">
|
||||
<span class="heading">{{ .lang.EndPage.finished }}</span>
|
||||
</div>
|
||||
<div class="row col flex center">
|
||||
<p class="content my-2">{{ .lang.EndPage.restartMessage }}</p>
|
||||
</div>
|
||||
<div class="row col flex center">
|
||||
<span class="button ~neutral @low back mr-4">{{ .lang.Strings.back }}</span>
|
||||
<span class="button ~urge @low" id="restart">{{ .lang.Strings.submit }}</span>
|
||||
<span class="button ~urge @low unfocused" id="refresh">{{ .lang.EndPage.refreshPage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/setup.js"></script>
|
||||
<script>
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.messages = JSON.parse({{ .messages }});
|
||||
</script>
|
||||
<script src="js/setup.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
|
||||
158
html/user.html
Normal file
@@ -0,0 +1,158 @@
|
||||
<html lang="en" class="light">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
|
||||
<script>
|
||||
window.URLBase = "{{ .urlBase }}";
|
||||
window.notificationsEnabled = {{ .notifications }};
|
||||
window.ombiEnabled = {{ .ombiEnabled }};
|
||||
window.langFile = JSON.parse({{ .language }});
|
||||
window.pwrEnabled = {{ .pwrEnabled }};
|
||||
window.linkResetEnabled = {{ .linkResetEnabled }};
|
||||
window.language = "{{ .langName }}";
|
||||
window.telegramEnabled = {{ .telegramEnabled }};
|
||||
window.telegramRequired = {{ .telegramRequired }};
|
||||
window.telegramUsername = {{ .telegramUsername }};
|
||||
window.telegramURL = {{ .telegramURL }};
|
||||
window.emailEnabled = {{ .emailEnabled }};
|
||||
window.emailRequired = {{ .emailRequired }};
|
||||
window.discordEnabled = {{ .discordEnabled }};
|
||||
window.discordRequired = {{ .discordRequired }};
|
||||
window.discordServerName = "{{ .discordServerName }}";
|
||||
window.discordInviteLink = {{ .discordInviteLink }};
|
||||
window.discordSendPINMessage = "{{ .discordSendPINMessage }}";
|
||||
window.matrixEnabled = {{ .matrixEnabled }};
|
||||
window.matrixRequired = {{ .matrixRequired }};
|
||||
window.matrixUserID = "{{ .matrixUser }}";
|
||||
window.validationStrings = JSON.parse({{ .validationStrings }});
|
||||
</script>
|
||||
{{ template "header.html" . }}
|
||||
<title>{{ .strings.myAccount }}</title>
|
||||
</head>
|
||||
<body class="max-w-full overflow-x-hidden section">
|
||||
<div id="modal-email" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3">
|
||||
<div class="content">
|
||||
<span class="heading mb-4 my-2"></span>
|
||||
<label class="label supra row m-1" for="modal-email-input">{{ .strings.emailAddress }}</label>
|
||||
<div class="row">
|
||||
<input type="email" class="col sm field ~neutral @low input" id="modal-email-input" placeholder="{{ .strings.emailAddress }}">
|
||||
</div>
|
||||
<button class="button ~urge @low supra full-width center lg my-2 modal-submit">{{ .strings.submit }}</button>
|
||||
</div>
|
||||
<div class="confirmation-required unfocused">
|
||||
<span class="heading mb-4">{{ .strings.confirmationRequired }} <span class="modal-close">×</span></span>
|
||||
<p class="content mb-4">{{ .strings.confirmationRequiredMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .pwrEnabled }}
|
||||
<div id="modal-pwr" class="modal">
|
||||
<div class="card relative mx-auto my-[10%] w-4/5 lg:w-1/3 ~neutral @low">
|
||||
<span class="heading">{{ .strings.resetPassword }}</span>
|
||||
<p class="content my-2">
|
||||
{{ if .linkResetEnabled }}
|
||||
{{ .strings.resetPasswordThroughLink }}
|
||||
{{ else }}
|
||||
{{ .strings.resetPasswordThroughJellyfin }}
|
||||
{{ end }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<input type="text" class="col sm field ~neutral @low input" id="pwr-address" placeholder="username | example@example.com | user#1234 | @user:host | @username">
|
||||
</div>
|
||||
{{ if .linkResetEnabled }}
|
||||
<span class="button ~info @low full-width center mt-4" id="pwr-submit">
|
||||
{{ .strings.submit }}
|
||||
</span>
|
||||
{{ else }}
|
||||
<a class="button ~info @low full-width center mt-4" href="{{ .jfLink }}" target="_blank">{{ .strings.continue }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "login-modal.html" . }}
|
||||
{{ template "account-linking.html" . }}
|
||||
<div id="notification-box"></div>
|
||||
<div class="top-4 left-4 absolute">
|
||||
<span class="dropdown" tabindex="0" id="lang-dropdown">
|
||||
<span class="button ~urge dropdown-button">
|
||||
<i class="ri-global-line"></i>
|
||||
<span class="ml-2 chev"></span>
|
||||
</span>
|
||||
<div class="dropdown-display">
|
||||
<div class="card ~neutral @low">
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-12h">
|
||||
<span>{{ .strings.time12h }}</span>
|
||||
</label>
|
||||
<label class="switch pb-4">
|
||||
<input type="radio" name="lang-time" id="lang-24h">
|
||||
<span>{{ .strings.time24h }}</span>
|
||||
</label>
|
||||
<div id="lang-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
|
||||
<span class="button ~critical @low mb-4 unfocused" id="logout-button">{{ .strings.logout }}</span>
|
||||
</div>
|
||||
<div class="top-4 right-4 absolute">
|
||||
<a class="button ~info unfocused" href="/" id="admin-back-button"><i class="ri-arrow-left-fill mr-2"></i>{{ .strings.admin }}</a>
|
||||
</div>
|
||||
<div class="page-container unfocused">
|
||||
<div class="card @low dark:~d_neutral mb-4" id="card-user">
|
||||
<span class="heading mb-2"></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{{ if index . "PageMessageEnabled" }}
|
||||
{{ if .PageMessageEnabled }}
|
||||
<div class="card @low dark:~d_neutral content" id="card-message">
|
||||
{{ .PageMessageContent }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="card @low dark:~d_neutral flex-col" id="card-contact">
|
||||
<span class="heading mb-2">{{ .strings.contactMethods }}</span>
|
||||
<div class="content flex justify-between flex-col h-100"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card @low dark:~d_neutral content" id="card-password">
|
||||
<span class="heading row mb-2">{{ .strings.changePassword }}</span>
|
||||
<div class="">
|
||||
<div class="my-2">
|
||||
<span class="label supra row">{{ .strings.passwordRequirementsHeader }}</span>
|
||||
<ul>
|
||||
{{ range $key, $value := .requirements }}
|
||||
<li class="" id="requirement-{{ $key }}" min="{{ $value }}">
|
||||
<span class="badge lg ~positive requirement-valid"></span> <span class="content requirement-content"></span>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<label class="label supra" for="user-old-password">{{ .strings.oldPassword }}</label>
|
||||
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-old-password" aria-label="{{ .strings.oldPassword }}">
|
||||
<label class="label supra" for="user-new-password">{{ .strings.newPassword }}</label>
|
||||
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-new-password" aria-label="{{ .strings.newPassword }}">
|
||||
|
||||
<label class="label supra" for="user-reenter-password">{{ .strings.reEnterPassword }}</label>
|
||||
<input type="password" class="input ~neutral @low mt-2 mb-4" placeholder="{{ .strings.password }}" id="user-reenter-new-password" aria-label="{{ .strings.reEnterPassword }}">
|
||||
<span class="button ~info @low full-width center mt-4" id="user-password-submit">
|
||||
{{ .strings.changePassword }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="card @low dark:~d_neutral unfocused" id="card-status">
|
||||
<span class="heading mb-2">{{ .strings.expiry }}</span>
|
||||
<aside class="aside ~warning user-expiry my-4"></aside>
|
||||
<div class="user-expiry-countdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ .urlBase }}/js/user.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 66 KiB |
BIN
images/demo.gif
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 1.9 MiB |
BIN
images/discord/1.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
images/discord/2.jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
images/discord/3.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
images/discord/4.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
images/discord/5.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
images/discord/6.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
images/discord/7.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
images/discord/8.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 83 KiB |
BIN
images/matrix/1.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
images/matrix/2.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
images/matrix/3.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
images/matrix/4.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
images/tg-settings.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
images/tg.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
images/thumb-white.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
203
images/thumb-white.svg
Normal file
|
After Width: | Height: | Size: 128 KiB |
42
internal.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// +build !external
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
)
|
||||
|
||||
const binaryType = "internal"
|
||||
|
||||
//go:embed data data/html data/web data/web/css data/web/js
|
||||
var loFS embed.FS
|
||||
|
||||
//go:embed lang/common lang/admin lang/email lang/form lang/setup lang/pwreset lang/telegram
|
||||
var laFS embed.FS
|
||||
|
||||
var langFS rewriteFS
|
||||
var localFS rewriteFS
|
||||
|
||||
type rewriteFS struct {
|
||||
fs embed.FS
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (l rewriteFS) Open(name string) (fs.File, error) { return l.fs.Open(l.prefix + name) }
|
||||
func (l rewriteFS) ReadDir(name string) ([]fs.DirEntry, error) { return l.fs.ReadDir(l.prefix + name) }
|
||||
func (l rewriteFS) ReadFile(name string) ([]byte, error) { return l.fs.ReadFile(l.prefix + name) }
|
||||
func FSJoin(elem ...string) string {
|
||||
out := ""
|
||||
for _, v := range elem {
|
||||
out += v + "/"
|
||||
}
|
||||
return out[:len(out)-1]
|
||||
}
|
||||
|
||||
func loadFilesystems() {
|
||||
langFS = rewriteFS{laFS, "lang/"}
|
||||
localFS = rewriteFS{loFS, "data/"}
|
||||
log.Println("Using internal storage")
|
||||
}
|
||||
8
jfa-go.service
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=An account management system for Jellyfin.
|
||||
|
||||
[Service]
|
||||
ExecStart={executable}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,7 +0,0 @@
|
||||
module github.com/hrfee/jfa-go/jfapi
|
||||
|
||||
go 1.15
|
||||
|
||||
replace github.com/hrfee/jfa-go/common => ../common
|
||||
|
||||
require github.com/hrfee/jfa-go/common v0.0.0-00010101000000-000000000000
|
||||
370
jfapi/jfapi.go
@@ -1,370 +0,0 @@
|
||||
package jfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hrfee/jfa-go/common"
|
||||
)
|
||||
|
||||
type serverInfo struct {
|
||||
LocalAddress string `json:"LocalAddress"`
|
||||
Name string `json:"ServerName"`
|
||||
Version string `json:"Version"`
|
||||
OS string `json:"OperatingSystem"`
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
|
||||
// Jellyfin represents a running Jellyfin instance.
|
||||
type Jellyfin struct {
|
||||
Server string
|
||||
client string
|
||||
version string
|
||||
device string
|
||||
deviceID string
|
||||
useragent string
|
||||
auth string
|
||||
header map[string]string
|
||||
ServerInfo serverInfo
|
||||
Username string
|
||||
password string
|
||||
Authenticated bool
|
||||
AccessToken string
|
||||
userID string
|
||||
httpClient *http.Client
|
||||
loginParams map[string]string
|
||||
userCache []map[string]interface{}
|
||||
CacheExpiry time.Time
|
||||
cacheLength int
|
||||
noFail bool
|
||||
Hyphens bool
|
||||
timeoutHandler common.TimeoutHandler
|
||||
}
|
||||
|
||||
// NewJellyfin returns a new Jellyfin object.
|
||||
func NewJellyfin(server, client, version, device, deviceID string, timeoutHandler common.TimeoutHandler, cacheTimeout int) (*Jellyfin, error) {
|
||||
jf := &Jellyfin{}
|
||||
jf.Server = server
|
||||
jf.client = client
|
||||
jf.version = version
|
||||
jf.device = device
|
||||
jf.deviceID = deviceID
|
||||
jf.useragent = fmt.Sprintf("%s/%s", client, version)
|
||||
jf.timeoutHandler = timeoutHandler
|
||||
jf.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\"", client, device, deviceID, version)
|
||||
jf.header = map[string]string{
|
||||
"Accept": "application/json",
|
||||
"Content-type": "application/json; charset=UTF-8",
|
||||
"X-Application": jf.useragent,
|
||||
"Accept-Charset": "UTF-8,*",
|
||||
"Accept-Encoding": "gzip",
|
||||
"User-Agent": jf.useragent,
|
||||
"X-Emby-Authorization": jf.auth,
|
||||
}
|
||||
jf.httpClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
infoURL := fmt.Sprintf("%s/System/Info/Public", server)
|
||||
req, _ := http.NewRequest("GET", infoURL, nil)
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
if err == nil {
|
||||
data, _ := ioutil.ReadAll(resp.Body)
|
||||
json.Unmarshal(data, &jf.ServerInfo)
|
||||
}
|
||||
jf.cacheLength = cacheTimeout
|
||||
jf.CacheExpiry = time.Now()
|
||||
return jf, nil
|
||||
}
|
||||
|
||||
// Authenticate attempts to authenticate using a username & password
|
||||
func (jf *Jellyfin) Authenticate(username, password string) (map[string]interface{}, int, error) {
|
||||
jf.Username = username
|
||||
jf.password = password
|
||||
jf.loginParams = map[string]string{
|
||||
"Username": username,
|
||||
"Pw": password,
|
||||
"Password": password,
|
||||
}
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(jf.loginParams)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// loginParams, _ := json.Marshal(jf.loginParams)
|
||||
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.Server)
|
||||
req, err := http.NewRequest("POST", url, buffer)
|
||||
defer jf.timeoutHandler()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return nil, resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
data, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
data = resp.Body
|
||||
}
|
||||
var respData map[string]interface{}
|
||||
json.NewDecoder(data).Decode(&respData)
|
||||
jf.AccessToken = respData["AccessToken"].(string)
|
||||
user := respData["User"].(map[string]interface{})
|
||||
jf.userID = respData["User"].(map[string]interface{})["Id"].(string)
|
||||
jf.auth = fmt.Sprintf("MediaBrowser Client=\"%s\", Device=\"%s\", DeviceId=\"%s\", Version=\"%s\", Token=\"%s\"", jf.client, jf.device, jf.deviceID, jf.version, jf.AccessToken)
|
||||
jf.header["X-Emby-Authorization"] = jf.auth
|
||||
jf.Authenticated = true
|
||||
return user, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) get(url string, params map[string]string) (string, int, error) {
|
||||
var req *http.Request
|
||||
if params != nil {
|
||||
jsonParams, _ := json.Marshal(params)
|
||||
req, _ = http.NewRequest("GET", url, bytes.NewBuffer(jsonParams))
|
||||
} else {
|
||||
req, _ = http.NewRequest("GET", url, nil)
|
||||
}
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.Authenticated {
|
||||
jf.Authenticated = false
|
||||
_, _, authErr := jf.Authenticate(jf.Username, jf.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := jf.get(url, params)
|
||||
return v1, v2, v3
|
||||
}
|
||||
}
|
||||
return "", resp.StatusCode, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var data io.Reader
|
||||
encoding := resp.Header.Get("Content-Encoding")
|
||||
switch encoding {
|
||||
case "gzip":
|
||||
data, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
data = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
io.Copy(buf, data)
|
||||
//var respData map[string]interface{}
|
||||
//json.NewDecoder(data).Decode(&respData)
|
||||
return buf.String(), resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func (jf *Jellyfin) post(url string, data map[string]interface{}, response bool) (string, int, error) {
|
||||
params, _ := json.Marshal(data)
|
||||
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(params))
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if resp.StatusCode == 401 && jf.Authenticated {
|
||||
jf.Authenticated = false
|
||||
_, _, authErr := jf.Authenticate(jf.Username, jf.password)
|
||||
if authErr == nil {
|
||||
v1, v2, v3 := jf.post(url, data, response)
|
||||
return v1, v2, v3
|
||||
}
|
||||
}
|
||||
return "", resp.StatusCode, err
|
||||
}
|
||||
if response {
|
||||
defer resp.Body.Close()
|
||||
var outData io.Reader
|
||||
switch resp.Header.Get("Content-Encoding") {
|
||||
case "gzip":
|
||||
outData, _ = gzip.NewReader(resp.Body)
|
||||
default:
|
||||
outData = resp.Body
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
io.Copy(buf, outData)
|
||||
return buf.String(), resp.StatusCode, nil
|
||||
}
|
||||
return "", resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) DeleteUser(userID string) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s", jf.Server, userID)
|
||||
req, _ := http.NewRequest("DELETE", url, nil)
|
||||
for name, value := range jf.header {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
resp, err := jf.httpClient.Do(req)
|
||||
defer jf.timeoutHandler()
|
||||
return resp.StatusCode, err
|
||||
}
|
||||
|
||||
// GetUsers returns all (visible) users on the Jellyfin instance.
|
||||
func (jf *Jellyfin) GetUsers(public bool) ([]map[string]interface{}, int, error) {
|
||||
var result []map[string]interface{}
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
if time.Now().After(jf.CacheExpiry) {
|
||||
if public {
|
||||
url := fmt.Sprintf("%s/users/public", jf.Server)
|
||||
data, status, err = jf.get(url, nil)
|
||||
} else {
|
||||
url := fmt.Sprintf("%s/users", jf.Server)
|
||||
data, status, err = jf.get(url, jf.loginParams)
|
||||
}
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
jf.userCache = result
|
||||
jf.CacheExpiry = time.Now().Add(time.Minute * time.Duration(jf.cacheLength))
|
||||
if id, ok := result[0]["Id"]; ok {
|
||||
if id.(string)[8] == '-' {
|
||||
jf.Hyphens = true
|
||||
}
|
||||
}
|
||||
return result, status, nil
|
||||
}
|
||||
return jf.userCache, 200, nil
|
||||
}
|
||||
|
||||
// UserByName returns the user corresponding to the provided username.
|
||||
func (jf *Jellyfin) UserByName(username string, public bool) (map[string]interface{}, int, error) {
|
||||
var match map[string]interface{}
|
||||
find := func() (map[string]interface{}, int, error) {
|
||||
users, status, err := jf.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user["Name"].(string) == username {
|
||||
return user, status, err
|
||||
}
|
||||
}
|
||||
return nil, status, err
|
||||
}
|
||||
match, status, err := find()
|
||||
if match == nil {
|
||||
jf.CacheExpiry = time.Now()
|
||||
match, status, err = find()
|
||||
}
|
||||
return match, status, err
|
||||
}
|
||||
|
||||
// UserByID returns the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) UserByID(userID string, public bool) (map[string]interface{}, int, error) {
|
||||
if jf.CacheExpiry.After(time.Now()) {
|
||||
for _, user := range jf.userCache {
|
||||
if user["Id"].(string) == userID {
|
||||
return user, 200, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if public {
|
||||
users, status, err := jf.GetUsers(public)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if user["Id"].(string) == userID {
|
||||
return user, status, nil
|
||||
}
|
||||
}
|
||||
return nil, status, err
|
||||
}
|
||||
var result map[string]interface{}
|
||||
var data string
|
||||
var status int
|
||||
var err error
|
||||
url := fmt.Sprintf("%s/users/%s", jf.Server, userID)
|
||||
data, status, err = jf.get(url, jf.loginParams)
|
||||
if err != nil || status != 200 {
|
||||
return nil, status, err
|
||||
}
|
||||
json.Unmarshal([]byte(data), &result)
|
||||
return result, status, nil
|
||||
}
|
||||
|
||||
// NewUser creates a new user with the provided username and password.
|
||||
func (jf *Jellyfin) NewUser(username, password string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/Users/New", jf.Server)
|
||||
stringData := map[string]string{
|
||||
"Name": username,
|
||||
"Password": password,
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
for key, value := range stringData {
|
||||
data[key] = value
|
||||
}
|
||||
response, status, err := jf.post(url, data, true)
|
||||
var recv map[string]interface{}
|
||||
json.Unmarshal([]byte(response), &recv)
|
||||
if err != nil || !(status == 200 || status == 204) {
|
||||
return nil, status, err
|
||||
}
|
||||
return recv, status, nil
|
||||
}
|
||||
|
||||
// SetPolicy sets the access policy for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) SetPolicy(userID string, policy map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Policy", jf.Server, userID)
|
||||
_, status, err := jf.post(url, policy, false)
|
||||
if err != nil || status != 200 {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// SetConfiguration sets the configuration (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) SetConfiguration(userID string, configuration map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/Users/%s/Configuration", jf.Server, userID)
|
||||
_, status, err := jf.post(url, configuration, false)
|
||||
return status, err
|
||||
}
|
||||
|
||||
// GetDisplayPreferences gets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) GetDisplayPreferences(userID string) (map[string]interface{}, int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID)
|
||||
data, status, err := jf.get(url, nil)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return nil, status, err
|
||||
}
|
||||
var displayprefs map[string]interface{}
|
||||
err = json.Unmarshal([]byte(data), &displayprefs)
|
||||
if err != nil {
|
||||
return nil, status, err
|
||||
}
|
||||
return displayprefs, status, nil
|
||||
}
|
||||
|
||||
// SetDisplayPreferences sets the displayPreferences (part of homescreen layout) for the user corresponding to the provided ID.
|
||||
func (jf *Jellyfin) SetDisplayPreferences(userID string, displayprefs map[string]interface{}) (int, error) {
|
||||
url := fmt.Sprintf("%s/DisplayPreferences/usersettings?userId=%s&client=emby", jf.Server, userID)
|
||||
_, status, err := jf.post(url, displayprefs, false)
|
||||
if err != nil || !(status == 204 || status == 200) {
|
||||
return status, err
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
226
lang.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package main
|
||||
|
||||
type langMeta struct {
|
||||
Name string `json:"name"`
|
||||
// Language to fall back on if strings are missing. Defaults to en-us.
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
type quantityString struct {
|
||||
Singular string `json:"singular"`
|
||||
Plural string `json:"plural"`
|
||||
}
|
||||
|
||||
type adminLangs map[string]adminLang
|
||||
|
||||
func (ls *adminLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type commonLangs map[string]commonLang
|
||||
|
||||
type commonLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
||||
}
|
||||
|
||||
type adminLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
||||
JSON string
|
||||
}
|
||||
|
||||
type userLangs map[string]userLang
|
||||
|
||||
func (ls *userLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type userLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
notificationsJSON string
|
||||
ValidationStrings map[string]quantityString `json:"validationStrings"`
|
||||
validationStringsJSON string
|
||||
QuantityStrings map[string]quantityString `json:"quantityStrings"`
|
||||
JSON string
|
||||
}
|
||||
|
||||
type pwrLangs map[string]pwrLang
|
||||
|
||||
func (ls *pwrLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type pwrLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
}
|
||||
|
||||
type emailLangs map[string]emailLang
|
||||
|
||||
func (ls *emailLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type emailLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
UserCreated langSection `json:"userCreated"`
|
||||
InviteExpiry langSection `json:"inviteExpiry"`
|
||||
PasswordReset langSection `json:"passwordReset"`
|
||||
UserDeleted langSection `json:"userDeleted"`
|
||||
UserDisabled langSection `json:"userDisabled"`
|
||||
UserEnabled langSection `json:"userEnabled"`
|
||||
InviteEmail langSection `json:"inviteEmail"`
|
||||
WelcomeEmail langSection `json:"welcomeEmail"`
|
||||
EmailConfirmation langSection `json:"emailConfirmation"`
|
||||
UserExpired langSection `json:"userExpired"`
|
||||
}
|
||||
|
||||
type setupLangs map[string]setupLang
|
||||
|
||||
type setupLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
StartPage langSection `json:"startPage"`
|
||||
EndPage langSection `json:"endPage"`
|
||||
General langSection `json:"general"`
|
||||
Updates langSection `json:"updates"`
|
||||
Language langSection `json:"language"`
|
||||
Login langSection `json:"login"`
|
||||
JellyfinEmby langSection `json:"jellyfinEmby"`
|
||||
Ombi langSection `json:"ombi"`
|
||||
Email langSection `json:"email"`
|
||||
Messages langSection `json:"messages"`
|
||||
Notifications langSection `json:"notifications"`
|
||||
WelcomeEmails langSection `json:"welcomeEmails"`
|
||||
PasswordResets langSection `json:"passwordResets"`
|
||||
InviteEmails langSection `json:"inviteEmails"`
|
||||
PasswordValidation langSection `json:"passwordValidation"`
|
||||
HelpMessages langSection `json:"helpMessages"`
|
||||
JSON string
|
||||
}
|
||||
|
||||
func (ls *setupLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ls))
|
||||
i := 0
|
||||
for key, lang := range *ls {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type telegramLangs map[string]telegramLang
|
||||
|
||||
type telegramLang struct {
|
||||
Meta langMeta `json:"meta"`
|
||||
Strings langSection `json:"strings"`
|
||||
}
|
||||
|
||||
func (ts *telegramLangs) getOptions() [][2]string {
|
||||
opts := make([][2]string, len(*ts))
|
||||
i := 0
|
||||
for key, lang := range *ts {
|
||||
opts[i] = [2]string{key, lang.Meta.Name}
|
||||
i++
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
type langSection map[string]string
|
||||
type tmpl map[string]string
|
||||
|
||||
func templateString(text string, vals tmpl) string {
|
||||
start, previousEnd := -1, -1
|
||||
out := ""
|
||||
for i := range text {
|
||||
if text[i] == '{' {
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
if start != -1 && text[i] == '}' {
|
||||
varName := text[start+1 : i]
|
||||
val, ok := vals[varName]
|
||||
if !ok {
|
||||
start = -1
|
||||
continue
|
||||
}
|
||||
out += text[previousEnd+1:start] + val
|
||||
previousEnd = i
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
if previousEnd != len(text)-1 {
|
||||
out += text[previousEnd+1:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (el langSection) template(field string, vals tmpl) string {
|
||||
text := el.get(field)
|
||||
return templateString(text, vals)
|
||||
}
|
||||
|
||||
func (el langSection) format(field string, vals ...string) string {
|
||||
text := el.get(field)
|
||||
start, previous := -1, -3
|
||||
out := ""
|
||||
val := 0
|
||||
for i := range text {
|
||||
if i == len(text)-2 { // Check if there's even enough space for a {n}
|
||||
break
|
||||
}
|
||||
if text[i:i+3] == "{n}" {
|
||||
start = i
|
||||
out += text[previous+3:start] + vals[val]
|
||||
previous = start
|
||||
val++
|
||||
if val == len(vals) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if previous+2 != len(text)-1 {
|
||||
out += text[previous+3:]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (el langSection) get(field string) string {
|
||||
t, ok := el[field]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return t
|
||||
}
|
||||
204
lang/admin/da-dk.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Dansk (DK)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invitationer",
|
||||
"accounts": "Konti",
|
||||
"settings": "Indstillinger",
|
||||
"inviteMonths": "Måneder",
|
||||
"inviteDays": "Dage",
|
||||
"inviteHours": "Timer",
|
||||
"inviteMinutes": "Minutter",
|
||||
"inviteNumberOfUses": "Antal anvendelser",
|
||||
"inviteDuration": "Invitations varighed",
|
||||
"warning": "Advarsel",
|
||||
"inviteInfiniteUsesWarning": "invitationer med uendelig brug kan blive misbrugt",
|
||||
"inviteSendToEmail": "Send til",
|
||||
"create": "Opret",
|
||||
"apply": "Anvend",
|
||||
"select": "Vælg",
|
||||
"name": "Navn",
|
||||
"date": "Dato",
|
||||
"updates": "Opdateringer",
|
||||
"update": "Opdatering",
|
||||
"download": "Hent",
|
||||
"search": "Søg",
|
||||
"advancedSettings": "Avanceret Indstillinger",
|
||||
"lastActiveTime": "Sidst Aktiv",
|
||||
"from": "Fra",
|
||||
"user": "Bruger",
|
||||
"userExpiry": "Brugerens Udløb",
|
||||
"userExpiryDescription": "En specificeret tid efter hver tilmelding, sletter/deaktiverer jfa-go kontoen. Du kan ændre denne adfærd i indstillingerne.",
|
||||
"aboutProgram": "Om",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "Ny Bruger",
|
||||
"profile": "Profil",
|
||||
"unknown": "Ukendt",
|
||||
"label": "Etiket",
|
||||
"announce": "Annoncere",
|
||||
"subject": "Emne",
|
||||
"message": "Meddelelse",
|
||||
"variables": "Variabler",
|
||||
"conditionals": "Betingelser",
|
||||
"preview": "Eksempel",
|
||||
"reset": "Nulstil",
|
||||
"donate": "Doner",
|
||||
"contactThrough": "Kontakt gennem:",
|
||||
"extendExpiry": "Forlæng udløb",
|
||||
"customizeMessages": "Tilpas Meddelelser",
|
||||
"customizeMessagesDescription": "Hvis du ikke vil bruge jfa-go's meddelelses skabeloner, kan du oprette din egen ved hjælp af Markdown.",
|
||||
"markdownSupported": "Markdown understøttes.",
|
||||
"modifySettings": "Rediger indstillinger",
|
||||
"modifySettingsDescription": "Anvend indstillinger fra en eksisterende profil, eller hent dem direkte fra en bruger.",
|
||||
"applyHomescreenLayout": "Anvend startskærmens layout",
|
||||
"sendDeleteNotificationEmail": "Send notifikations meddelelse",
|
||||
"sendDeleteNotifiationExample": "Din konto er blevet slettet.",
|
||||
"settingsRestart": "Genstart",
|
||||
"settingsRestarting": "Genstarter…",
|
||||
"settingsRestartRequired": "Genstart nødvendig",
|
||||
"settingsRestartRequiredDescription": "En genstart er nødvendig for at anvende nogle indstillinger du har ændret. Genstart nu eller senere?",
|
||||
"settingsApplyRestartLater": "Anvend, genstart senere",
|
||||
"settingsApplyRestartNow": "Anvend & genstart",
|
||||
"settingsApplied": "Indstillingerne anvendt.",
|
||||
"settingsRefreshPage": "Opdater siden om få sekunder.",
|
||||
"settingsRequiredOrRestartMessage": "Bemærk: {n} angiver et obligatorisk felt, {n} angiver at ændringer kræver genstart.",
|
||||
"settingsSave": "Gem",
|
||||
"ombiUserDefaults": "Ombi bruger standarder",
|
||||
"ombiUserDefaultsDescription": "Opret en Ombi bruger og konfigurer den, vælg den derefter nedenfor. Brugerens indstillinger/tilladelser gemmes og anvendes på nye Ombi brugere oprettet af jfa-go når denne profil er valgt.",
|
||||
"userProfiles": "Bruger Profiler",
|
||||
"userProfilesDescription": "Profiler anvendes på brugere når de opretter en konto. En profil inkluderer adgangsrettigheder til biblioteket og layout på startskærmen.",
|
||||
"userProfilesIsDefault": "Standard",
|
||||
"userProfilesLibraries": "Biblioteker",
|
||||
"addProfile": "Tilføj Profil",
|
||||
"addProfileDescription": "Opret en Jellyfin bruger og konfigurer den, vælg den derefter nedenfor. Når denne profil anvendes på en invitation, oprettes nye brugere med indstillingerne.",
|
||||
"addProfileNameOf": "Profil Navn",
|
||||
"addProfileStoreHomescreenLayout": "Gem startskærmens layout",
|
||||
"inviteNoUsersCreated": "Ingen endnu!",
|
||||
"inviteUsersCreated": "Oprettet brugere",
|
||||
"inviteNoProfile": "Ingen Profil",
|
||||
"inviteDateCreated": "Oprettet",
|
||||
"inviteRemainingUses": "Resterende anvendelser",
|
||||
"inviteNoInvites": "Ingen",
|
||||
"inviteExpiresInTime": "Udløber om {n}",
|
||||
"notifyEvent": "Meddel den:",
|
||||
"notifyInviteExpiry": "Ved udløb",
|
||||
"notifyUserCreation": "Ved oprettelse af brugere",
|
||||
"sendPIN": "Bed brugeren om at sende pinkoden nedenfor til botten.",
|
||||
"searchDiscordUser": "Begynd at skrive Discord brugernavnet for at finde brugeren.",
|
||||
"findDiscordUser": "Find Discord bruger",
|
||||
"linkMatrixDescription": "Indtast brugernavnet og adgangskoden til den bruger der skal bruges som en bot. Når indsendt, genstarter appen.",
|
||||
"matrixHomeServer": "Hjemme server adresse",
|
||||
"saveAsTemplate": "Gem som skabelon",
|
||||
"templates": "Skabeloner",
|
||||
"deleteTemplate": "Slet skabelon",
|
||||
"templateEnterName": "Indtast et navn for at gemme denne skabelon.",
|
||||
"ombiProfile": "Ombi bruger profil",
|
||||
"setExpiry": "Sæt udløb",
|
||||
"logs": "Log",
|
||||
"sendPWR": "Send Nulstilling af Adgangskode",
|
||||
"sendPWRManual": "Brugeren {n} har ingen kontaktinformation, tryk kopier for at få et link du kan sende til dem.",
|
||||
"sendPWRSuccess": "Link til nulstilling af adgangskode sendt.",
|
||||
"sendPWRSuccessManual": "Hvis brugeren ikke er modtaget den, så tryk på kopier for manuelt at sende et link til dem.",
|
||||
"sendPWRValidFor": "Dette link er gyldigt i 30m.",
|
||||
"accessJFA": "Få adgang til jfa-go",
|
||||
"accessJFASettings": "Kan ikke ændres, da enten \"Kun administrator\" eller \"Tillad alle\" er blevet indstillet i Indstillinger > Generelt."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Ændret e-mail adresse på {n}.",
|
||||
"userCreated": "Bruger {n} oprettet.",
|
||||
"createProfile": "Oprettede profil {n}.",
|
||||
"saveSettings": "Indstillingerne blev gemt",
|
||||
"saveEmail": "E-mail gemt.",
|
||||
"sentAnnouncement": "Meddelelse sendt.",
|
||||
"setOmbiDefaults": "Ombi standarder gemt.",
|
||||
"updateApplied": "Opdatering anvendt, genstart.",
|
||||
"updateAppliedRefresh": "Opdatering anvendt, genindlæs venligst siden.",
|
||||
"telegramVerified": "Telegram konto verificeret.",
|
||||
"accountConnected": "Konto tilsluttet.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Indstillingerne blev anvendt, men anvendelse af startskærmens layout mislykkedes muligvis.",
|
||||
"errorHomescreenAppliedNoSettings": "Startskærmens layout blev anvendt, men anvendelsen af indstillingerne mislykkedes muligvis.",
|
||||
"errorSettingsFailed": "Ansøgningen mislykkedes.",
|
||||
"errorSaveEmail": "Kunne ikke gemme e-mail.",
|
||||
"errorBlankFields": "Felter blev efterladt tomme",
|
||||
"errorDeleteProfile": "Kunne ikke slette profilen {n}",
|
||||
"errorLoadProfiles": "Profiler kunne ikke indlæses.",
|
||||
"errorCreateProfile": "Kunne ikke oprette profilen {n}",
|
||||
"errorSetDefaultProfile": "Standard profilen kunne ikke indstilles.",
|
||||
"errorLoadUsers": "Kunne ikke indlæse brugere.",
|
||||
"errorLoadSettings": "Indstillingerne kunne ikke indlæses.",
|
||||
"errorSetOmbiDefaults": "Ombi standarderne kunne ikke gemmes.",
|
||||
"errorLoadOmbiUsers": "Kunne ikke indlæse ombi brugere.",
|
||||
"errorChangedEmailAddress": "Kunne ikke ændre e-mail adressen på {n}.",
|
||||
"errorFailureCheckLogs": "Mislykkedes (tjek konsol/logfiler)",
|
||||
"errorPartialFailureCheckLogs": "Delvis fejl (tjek konsol/logfiler)",
|
||||
"errorUserCreated": "Kunne ikke oprette bruger {n}.",
|
||||
"errorSendWelcomeEmail": "Kunne ikke sende velkomst meddelelse (tjek konsol/logfiler",
|
||||
"errorApplyUpdate": "Kunne ikke anvende opdateringen, prøv manuelt.",
|
||||
"errorCheckUpdate": "Kunne ikke kontrollere for opdatering.",
|
||||
"updateAvailable": "En ny opdatering er tilgængelig, tjek indstillingerne.",
|
||||
"noUpdatesAvailable": "Ingen nye opdateringer tilgængelige.",
|
||||
"savedAnnouncement": "Meddelelse gemt.",
|
||||
"setOmbiProfile": "Gemt i ombi profilen.",
|
||||
"errorSetOmbiProfile": "Ombi profilen kunne ikke gemmes."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Rediger indstillinger for {n} bruger",
|
||||
"plural": "Rediger indstillinger for {n} brugere"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Slet {n} bruger",
|
||||
"plural": "Slet {n} brugere"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Deaktiver {n} bruger",
|
||||
"plural": "Deaktiver {n} brugere"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Genaktiver {n} bruger",
|
||||
"plural": "Genaktiver {n} brugere"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Tilføj bruger",
|
||||
"plural": "Tilføj brugere"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Slet bruger",
|
||||
"plural": "Slet brugere"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Slettede {n} bruger.",
|
||||
"plural": "Slettede {n} brugere."
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "Deaktiveret {n} bruger.",
|
||||
"plural": "Deaktiverede {n} brugere."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Aktiveret {n} bruger.",
|
||||
"plural": "Aktiveret {n} brugere."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Annoncer til {n} bruger",
|
||||
"plural": "Annoncer til {n} brugere"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Anvendte indstillinger til {n} bruger.",
|
||||
"plural": "Anvendte indstillinger til {n} brugere."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Forlæng udløbet for {n} bruger",
|
||||
"plural": "Forlæng udløbet for {n} brugere"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Forlængede udløb for {n} bruger.",
|
||||
"plural": "Forlængede udløb for {n} brugere."
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Indstil udløb for {n} bruger",
|
||||
"plural": "Indstil udløb for {n} brugere"
|
||||
}
|
||||
}
|
||||
}
|
||||
204
lang/admin/de-de.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invites",
|
||||
"accounts": "Konten",
|
||||
"settings": "Einstellungen",
|
||||
"inviteDays": "Tage",
|
||||
"inviteHours": "Stunden",
|
||||
"inviteMinutes": "Minuten",
|
||||
"inviteNumberOfUses": "Anzahl Verwendungen",
|
||||
"warning": "Warnung",
|
||||
"inviteInfiniteUsesWarning": "Invites mit unendlich vielen Verwendungen können missbräuchlich verwendet werden",
|
||||
"inviteSendToEmail": "Senden an",
|
||||
"create": "Erstellen",
|
||||
"apply": "Anwenden",
|
||||
"name": "Name",
|
||||
"date": "Datum",
|
||||
"lastActiveTime": "Zuletzt aktiv",
|
||||
"from": "Von",
|
||||
"user": "Benutzer",
|
||||
"aboutProgram": "Über",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "Neuer Benutzer",
|
||||
"profile": "Profil",
|
||||
"unknown": "Unbekannt",
|
||||
"modifySettings": "Einstellungen ändern",
|
||||
"modifySettingsDescription": "Wende Einstellungen von einem bestehenden Profil an, oder beziehe sie direkt von einem Benutzer.",
|
||||
"applyHomescreenLayout": "Startbildschirmlayout anwenden",
|
||||
"sendDeleteNotificationEmail": "Benachrichtigung senden",
|
||||
"sendDeleteNotifiationExample": "Dein Konto wurde gelöscht.",
|
||||
"settingsRestartRequired": "Neustart erforderlich",
|
||||
"settingsRestartRequiredDescription": "Ein Neustart ist notwendig, um einige Einstellungen anzuwenden, die du geändert hast. Jetzt oder später neu starten?",
|
||||
"settingsApplyRestartLater": "Anwenden, später neu starten",
|
||||
"settingsApplyRestartNow": "Anwenden & neu starten",
|
||||
"settingsApplied": "Einstellungen angewendet.",
|
||||
"settingsRefreshPage": "Aktualisiere die Seite in ein paar Sekunden.",
|
||||
"settingsRequiredOrRestartMessage": "Hinweis: {n} zeigt ein erforderliches Feld an, {n} zeigt an, dass Änderungen einen Neustart erfordern.",
|
||||
"settingsSave": "Speichern",
|
||||
"ombiUserDefaults": "Ombi-Benutzerstandardeinstellungen",
|
||||
"ombiUserDefaultsDescription": "Erstelle einen Ombi-Benutzer, konfiguriere ihn und wähle ihn dann unten aus. Seine Einstellungen/Berechtigungen werden gespeichert und auf neue Ombi-Benutzer, welche von jfa-go erstellt werden, angewendet, sofern dieses Profil ausgewählt ist.",
|
||||
"userProfiles": "Benutzerprofile",
|
||||
"userProfilesDescription": "Profile werden auf Benutzer angewendet, wenn sie ein Konto erstellen. Ein Profil beinhaltet Bibliothekszugriffsrechte und das Startbildschirmlayout.",
|
||||
"userProfilesIsDefault": "Standard",
|
||||
"userProfilesLibraries": "Bibliotheken",
|
||||
"addProfile": "Profil hinzufügen",
|
||||
"addProfileDescription": "Erstelle einen Jellyfin-Benutzer und konfiguriere ihn, dann wähle ihn unten aus. Wenn dieses Profil auf einen Invite angewendet ist, werden neue Benutzer mit den Einstellungen erstellt.",
|
||||
"addProfileNameOf": "Profilname",
|
||||
"addProfileStoreHomescreenLayout": "Startbildschirmlayout speichern",
|
||||
"inviteNoUsersCreated": "Noch keine!",
|
||||
"inviteUsersCreated": "Erstellte Benutzer",
|
||||
"inviteNoProfile": "Kein Profil",
|
||||
"inviteDateCreated": "Erstellt",
|
||||
"inviteRemainingUses": "Verbleibende Verwendungen",
|
||||
"inviteNoInvites": "Keine",
|
||||
"inviteExpiresInTime": "Läuft in {n} ab",
|
||||
"notifyEvent": "Benachrichtigen bei:",
|
||||
"notifyInviteExpiry": "Bei Ablauf",
|
||||
"notifyUserCreation": "Bei Benutzererstellung",
|
||||
"label": "Label",
|
||||
"settingsRestarting": "Neustart…",
|
||||
"settingsRestart": "Neustart",
|
||||
"variables": "Variablen",
|
||||
"preview": "Vorschau",
|
||||
"reset": "Zurücksetzen",
|
||||
"customizeMessages": "Benachrichtigungen anpassen",
|
||||
"customizeMessagesDescription": "Wenn du jfa-go's E-Mail-Vorlagen nicht benutzen willst, kannst du deinen eigenen unter Verwendung von Markdown erstellen.",
|
||||
"announce": "Ankündigen",
|
||||
"subject": "Betreff",
|
||||
"message": "Nachricht",
|
||||
"markdownSupported": "Markdown wird unterstützt.",
|
||||
"advancedSettings": "Erweiterte Einstellungen",
|
||||
"search": "Suchen",
|
||||
"userExpiry": "Benutzer Ablaufdatum",
|
||||
"inviteDuration": "Invite Dauer",
|
||||
"userExpiryDescription": "Eine bestimmte Zeit nach der Anmeldung wird jfa-go das Konto löschen/deaktivieren. Du kannst dieses Verhalten in den Einstellungen ändern.",
|
||||
"download": "Herunterladen",
|
||||
"update": "Aktualisieren",
|
||||
"updates": "Aktualisierungen",
|
||||
"extendExpiry": "Ablaufdatum verlängern",
|
||||
"donate": "Spenden",
|
||||
"conditionals": "Bedingungen",
|
||||
"contactThrough": "Kontakt über:",
|
||||
"sendPIN": "Bitte den Benutzer, die unten stehende PIN an den Bot zu senden.",
|
||||
"inviteMonths": "Monate",
|
||||
"select": "Auswählen",
|
||||
"searchDiscordUser": "Gib den Discord-Benutzername ein, um den Benutzer zu finden.",
|
||||
"findDiscordUser": "Suche Discord-Benutzer",
|
||||
"linkMatrixDescription": "Gib den Benutzernamen und das Passwort des Benutzers ein, der als Bot verwendet werden soll. Nach dem Absenden wird die App neu gestartet.",
|
||||
"matrixHomeServer": "Adresse des Homeservers",
|
||||
"templates": "Vorlagen",
|
||||
"ombiProfile": "Ombi-Benutzerprofil",
|
||||
"accessJFA": "jfa-go Zugriff",
|
||||
"sendPWRValidFor": "Der Link ist 30m gültig.",
|
||||
"logs": "Logdaten",
|
||||
"setExpiry": "Ablauf setzen",
|
||||
"sendPWRSuccess": "Link zur Passwortrücksetzung versandt.",
|
||||
"sendPWRSuccessManual": "Falls der Benutzer ihn nicht erhalten hat, klicke \"Kopieren\" und sende ihm den Link manuell.",
|
||||
"sendPWR": "Sende Passwortrücksetzung",
|
||||
"sendPWRManual": "Benutzer {n} hat keine Kontaktmöglichkeit hinterlegt. Klicke \"Kopieren\" um einen Link zu erhalten, den du dem Benutzer manuell senden kannst.",
|
||||
"accessJFASettings": "Kann nicht geändert werden, da entweder \"Nur Admin-Benutzer\" oder \"Erlaube allen Jellyfin-Nutzern sich anzumelden\" in Einstellungen > Allgemein aktiviert ist.",
|
||||
"saveAsTemplate": "Als Vorlage speichern",
|
||||
"deleteTemplate": "Vorlage löschen",
|
||||
"templateEnterName": "Gebe einen Namen ein, um diese Vorlage zu speichern."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-Mail-Adresse von {n} geändert.",
|
||||
"userCreated": "Benutzer {n} erstellt.",
|
||||
"createProfile": "Profil {n} erstellt.",
|
||||
"saveSettings": "Einstellungen wurden gespeichert",
|
||||
"setOmbiDefaults": "Ombi-Standardeinstellungen gespeichert.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Einstellungen wurden angewendet, aber die Anwendung des Startbildschirmlayouts ist möglicherweise fehlgeschlagen.",
|
||||
"errorHomescreenAppliedNoSettings": "Startbildschirmlayout wurde angewendet, aber die Anwendung der Einstellungen ist möglicherweise fehlgeschlagen.",
|
||||
"errorSettingsFailed": "Anwendung ist fehlgeschlagen.",
|
||||
"errorBlankFields": "Felder wurden nicht ausgefüllt",
|
||||
"errorDeleteProfile": "Fehler beim Löschen des Profils {n}",
|
||||
"errorLoadProfiles": "Fehler beim Laden der Profile.",
|
||||
"errorCreateProfile": "Fehler beim Erstellen des Profils {n}",
|
||||
"errorSetDefaultProfile": "Fehler beim Setzen des Standardprofils.",
|
||||
"errorLoadUsers": "Fehler beim Laden der Benutzer.",
|
||||
"errorLoadSettings": "Fehler beim Laden der Einstellungen.",
|
||||
"errorSetOmbiDefaults": "Fehler beim Speichern der Ombi-Standardeinstellungen.",
|
||||
"errorLoadOmbiUsers": "Fehler beim Laden der Ombi-Benutzer.",
|
||||
"errorChangedEmailAddress": "E-Mail-Adresse von {n} konnte nicht geändert werden.",
|
||||
"errorFailureCheckLogs": "Fehlgeschlagen (überprüfe die Konsole/Logs)",
|
||||
"errorPartialFailureCheckLogs": "Teilweiser Fehlschlag (überprüfe die Konsole/Logs)",
|
||||
"errorUserCreated": "Fehler beim Erstellen des Benutzers {n}.",
|
||||
"errorSendWelcomeEmail": "Fehler beim Senden der Willkommensnachricht (überprüfe die Konsole/Logs)",
|
||||
"saveEmail": "E-Mail gespeichert.",
|
||||
"errorSaveEmail": "Fehler beim Speichern der E-Mail.",
|
||||
"sentAnnouncement": "Ankündigung gesendet.",
|
||||
"updateApplied": "Aktualisierung angewendet, bitte neu starten.",
|
||||
"errorApplyUpdate": "Fehler beim Anwenden der Aktualisierung, versuche es manuell.",
|
||||
"errorCheckUpdate": "Fehler beim Suchen nach Aktualisierungen.",
|
||||
"updateAvailable": "Eine neue Aktualisierung ist verfügbar, überprüfe die Einstellungen.",
|
||||
"noUpdatesAvailable": "Keinen neuen Aktualisierungen verfügbar.",
|
||||
"updateAppliedRefresh": "Update angewendet, bitte aktualisieren.",
|
||||
"telegramVerified": "Telegram-Konto verifiziert.",
|
||||
"accountConnected": "Konto verbunden.",
|
||||
"savedAnnouncement": "Ankündigung gespeichert.",
|
||||
"errorSetOmbiProfile": "Ombi-Profil konnte nicht gespeichert werden.",
|
||||
"setOmbiProfile": "Ombi-Profil gespeichert."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Einstellungen für {n} Benutzer ändern",
|
||||
"plural": "Einstellungen für {n} Benutzer ändern"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "{n} Benutzer löschen",
|
||||
"plural": "{n} Benutzer löschen"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Benutzer hinzufügen",
|
||||
"plural": "Benutzer hinzufügen"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Benutzer löschen",
|
||||
"plural": "Benutzer löschen"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "{n} Benutzer gelöscht.",
|
||||
"plural": "{n} Benutzer gelöscht."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Einstellungen auf {n} Benutzer angewendet.",
|
||||
"plural": "Einstellungen auf {n} Benutzer angewendet."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "{n} Benutzer mitteilen",
|
||||
"plural": "{n} Benutzern mitteilen"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Ablaufdatum für {n} Benutzer verlängern",
|
||||
"plural": "Ablaufdatum für {n} Benutzer verlängern"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Ablaufdatum für {n} Benutzer verlängern.",
|
||||
"plural": "Ablaufdatum für {n} Benutzer verlängern."
|
||||
},
|
||||
"disabledUser": {
|
||||
"plural": "Benutzer {n} Deaktiviert.",
|
||||
"singular": "Benutzer {n} Deaktiviert."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Benutzer {n} Aktiviert.",
|
||||
"plural": "Benutzer {n} Aktiviert."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Benutzer {n} deaktivieren",
|
||||
"plural": "Deaktiviere {n} Benutzer"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Benutzer {n} wieder aktivieren",
|
||||
"plural": "Benutzer {n} wieder aktivieren"
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Ablauf für {n} Benutzer setzen",
|
||||
"plural": "Ablauf für {n} Benutzer setzen"
|
||||
}
|
||||
}
|
||||
}
|
||||
171
lang/admin/el-gr.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Ελληνικά (GR)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Προσκλήσεις",
|
||||
"accounts": "Λογαριασμοί",
|
||||
"settings": "Ρυθμίσεις",
|
||||
"inviteDays": "Ημέρες",
|
||||
"inviteHours": "Ώρες",
|
||||
"inviteMinutes": "Λεπτά",
|
||||
"inviteNumberOfUses": "Αριθμός χρήσεων",
|
||||
"warning": "Προσοχή",
|
||||
"inviteInfiniteUsesWarning": "μπορεί να γίνει κατάχρηση των προσκλήσεων με άπειρες χρήσεις",
|
||||
"inviteSendToEmail": "Αποστολή σε",
|
||||
"create": "Δημιουργία",
|
||||
"apply": "Εφαρμογή",
|
||||
"name": "Όνομα",
|
||||
"date": "Ημερομηνία",
|
||||
"lastActiveTime": "Τελευταία Ενεργός",
|
||||
"from": "Απο",
|
||||
"user": "Χρήστης",
|
||||
"aboutProgram": "Σχετικά",
|
||||
"version": "Έκδοση",
|
||||
"commitNoun": "Καταχώρηση",
|
||||
"newUser": "Νέος Χρήστης",
|
||||
"profile": "Προφίλ",
|
||||
"unknown": "Άγνωστο",
|
||||
"label": "Ετικέτα",
|
||||
"modifySettings": "Επεξεργασία Ρυθμίσεων",
|
||||
"modifySettingsDescription": "Εφαρμογή ρυθμίσεων απο υπάρχον προφίλ ή απευθείας απο χρήστη.",
|
||||
"applyHomescreenLayout": "Εφαρμογή δομής αρχικής οθόνης",
|
||||
"sendDeleteNotificationEmail": "Αποστολή ενημερωτικού email",
|
||||
"sendDeleteNotifiationExample": "Ο λογαριασμός σας έχει διαγραφεί.",
|
||||
"settingsRestart": "Επανεκίνηση",
|
||||
"settingsRestarting": "Επανεκινεί…",
|
||||
"settingsRestartRequired": "Απαιτείται επανεκκίνηση",
|
||||
"settingsRestartRequiredDescription": "Απαιτείται επανεκκίνηση για να εφαρμοστούν κάποιες απο τις ρυθμίσεις που αλλάξατε. Επανεκκίνηση τώρα ή αργότερα?",
|
||||
"settingsApplyRestartLater": "Εφαρμογή, επανεκκίνηση αργότερα",
|
||||
"settingsApplyRestartNow": "Εφαρμογή και επανεκκίνηση",
|
||||
"settingsApplied": "Οι ρυθμίσεις εφαρμόστηκαν.",
|
||||
"settingsRefreshPage": "Επαναφορτόστε την σελίδα σε μερικά δεύτερα.",
|
||||
"settingsRequiredOrRestartMessage": "Σημείωση: {n} δηλώνει υποχρεωτικό πεδίο, {n} δηλώνει αλλαγές που απαιτούν επανεκκίνηση.",
|
||||
"settingsSave": "Αποθήκευση",
|
||||
"ombiUserDefaults": "Προεπιλογές χρήστη Ombi",
|
||||
"ombiUserDefaultsDescription": "Δημιουργήστε έναν χρήστη Ombi και ρυθμίστε τον, μετά επιλέξτε τον απο κάτω. Οι ρυθμίσεις/άδειες θα αποθηκευτούν και εφαρμοστούν σε νέους χρήστες Ombi που έχουν δημιουργηθεί απο το jfa-go",
|
||||
"userProfiles": "Προφιλ Χρήστη",
|
||||
"userProfilesDescription": "Τα προφίλ εφαρμόζονται στους χρήστες όταν δημιουργούν λογαριασμό. Το προφίλ περιέχει δικαιώματα στις βιβλιοθήκες και δομή αρχικής οθόνης.",
|
||||
"userProfilesIsDefault": "Προκαθορισμένα",
|
||||
"userProfilesLibraries": "Βιβλιοθήκες",
|
||||
"addProfile": "Προσθήκη Προφίλ",
|
||||
"addProfileDescription": "Δημιουργήστε χρήστη Jellyfin και ρυθμίστε τον, μετά επιλέξτε τον παρακάτω. Όταν αυτό το προφίλ επιλέγεται σε μία πρόσκληση, τότε οι νέοι χρήστες θα δημιουργηθούν με αυτές τις ρυθμίσεις.",
|
||||
"addProfileNameOf": "Όνομα Προφίλ",
|
||||
"addProfileStoreHomescreenLayout": "Αποθήκευση αρχικής οθόνης",
|
||||
"inviteNoUsersCreated": "Τίποτα ακόμα!",
|
||||
"inviteUsersCreated": "Δημιουργηθέντες χρήστες",
|
||||
"inviteNoProfile": "Κανένα Προφίλ",
|
||||
"inviteDateCreated": "Δημιουργηθέντα",
|
||||
"inviteRemainingUses": "Εναπομείναντες χρήσεις",
|
||||
"inviteNoInvites": "Καμία",
|
||||
"inviteExpiresInTime": "Λήγει σε {n}",
|
||||
"notifyEvent": "Ενημέρωση όταν:",
|
||||
"notifyInviteExpiry": "Στην λήξη",
|
||||
"notifyUserCreation": "Στην δημιουργία χρήστη",
|
||||
"variables": "Μεταβλητές",
|
||||
"preview": "Προεπισκόπηση",
|
||||
"reset": "Επαναφορά",
|
||||
"customizeMessages": "Παραμετροποίηση Emails",
|
||||
"advancedSettings": "Προχωρημένες Ρυθμίσεις",
|
||||
"customizeMessagesDescription": "Αν δεν θέλετε να ζρησιμοποιήσετε τα πρότυπα email του jfa-go, μπορείτε να δημιουργήσετε τα δικά σας με χρήση Markdown.",
|
||||
"updates": "Ενημερώσεις",
|
||||
"update": "Ενημέρωση",
|
||||
"download": "Λήψη",
|
||||
"search": "Αναζήτηση",
|
||||
"inviteDuration": "Διάρκεια Πρόσκλησης",
|
||||
"userExpiry": "Λήξη Χρήστη",
|
||||
"userExpiryDescription": "Μετά απο ένα καθορισμένο χρόνο μετά απο κάθε εγγραφή, το jfa-go θα διαγράφει/απενεργοποιεί τον λογαριασμό. Μπορείτε να αλλάξετε αυτή την συμπεριφορά στις ρυθμίσεις.",
|
||||
"announce": "Ανακοίνωση",
|
||||
"subject": "Θέμα Email",
|
||||
"message": "Μήνυμα",
|
||||
"extendExpiry": "Παράταση λήξης",
|
||||
"markdownSupported": "Το Markdown υποστυρίζεται.",
|
||||
"inviteMonths": "Μήνες"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Αλλαγή {n} διεύθυνσεων email.",
|
||||
"userCreated": "Δημιουργήθηκε ο {n} χρήστης.",
|
||||
"createProfile": "Δημιουργήθηκε το {n} προφίλ.",
|
||||
"saveSettings": "Οι ρυθμίσεις αποθηκεύτηκαν",
|
||||
"setOmbiDefaults": "Αποθηκεύτηκαν οι προκαθορισμένες ρυθμίσεις του ombi.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Οι ρυθμίσεις αποθηκεύτηκαν, αλλά η καταχώρηση δομής αρχικής οθόνης ίσως απέτυχε.",
|
||||
"errorHomescreenAppliedNoSettings": "Η δομή αρχικής οθόνης εφαρμόστηκε, αλλά οι ρυθμίσεις ίσως απέτυχαν.",
|
||||
"errorSettingsFailed": "Η εφαρμογή απέτυχε.",
|
||||
"errorBlankFields": "Τα πεφία ήταν κενά",
|
||||
"errorDeleteProfile": "Αποτυχία διαγραφής του προφίλ {n}",
|
||||
"errorLoadProfiles": "Αποτυχία φόρτωσης των προφίλ.",
|
||||
"errorCreateProfile": "Αποτυχία δημιουργίας του προφίλ {n}",
|
||||
"errorSetDefaultProfile": "Αποτυχία ορισμού του προκαθορισμένου προφίλ.",
|
||||
"errorLoadUsers": "Αποτυχία φόρτωσης χρηστών.",
|
||||
"errorLoadSettings": "Αποτυχία φόρτωσης ρυθμίσεων.",
|
||||
"errorSetOmbiDefaults": "Αποτυχία αποθήκευσης προκαθορισμένων ρυθμίσεων για το Ombi.",
|
||||
"errorLoadOmbiUsers": "Αποτυχία φόρτωσης χρηστών Ombi.",
|
||||
"errorChangedEmailAddress": "Αποτυχία αλλαγής email του {n}.",
|
||||
"errorFailureCheckLogs": "Αποτυχία (ελέγξτε κονσόλα/καταγραφές)",
|
||||
"errorPartialFailureCheckLogs": "Μερική αποτυχία (ελέγξτε κονσόλα/καταγραφές)",
|
||||
"errorUserCreated": "Αποτυχία δημιουργίας του χρήστη {n}.",
|
||||
"errorSendWelcomeEmail": "Αποτυχία αποστολής email καλωσορίσματος (ελέγξτε κονσόλα/καταγραφές)",
|
||||
"saveEmail": "Το email αποθηκεύτηκε.",
|
||||
"sentAnnouncement": "Ανακοίνωση εστάλη.",
|
||||
"updateApplied": "Η ενημέρωση εφαρμόστηκε, παρακαλώ επανεκκινήστε.",
|
||||
"errorSaveEmail": "Αποτυχία αποθήκευσης του email.",
|
||||
"errorApplyUpdate": "Αποτυχία εγκατάστασης ενημέρωσης, προσπαθήστε χειροκίνητα.",
|
||||
"errorCheckUpdate": "Αποτυχία ελέγχου για ενημερώσεις.",
|
||||
"updateAvailable": "Μια νέα ενημέρωση είναι διαθέσιμη, ελέγξτε τις ρυθμίσεις.",
|
||||
"noUpdatesAvailable": "Δεν υπάρχουν διαθέσιμες ενημερώσεις."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Επεξεργασία Ρυθμίσεων για {n} χρήστη",
|
||||
"plural": "Επεξεργασία Ρυθμίσεων για {n} χρήστες"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Διαγραφή {n} χρήστη",
|
||||
"plural": "Διαγραφή {n} χρηστών"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Προσθήκη χρήστη",
|
||||
"plural": "Προσθήκη χρηστών"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Διαγραφή Χρήστη",
|
||||
"plural": "Διαγραφή Χρηστών"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Διαγράφη {n} χρήστη.",
|
||||
"plural": "Διαγράφησαν {n} χρήστες."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Εφαρμογή ρυθμίσεων σε {n} χρήστη.",
|
||||
"plural": "Εφαρμογή ρυθμίσεων σε {n} χρήστες."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Ανακοίνωση σε {n} χρήστη",
|
||||
"plural": "Ανακοίνωση σε {n} χρήστες"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"plural": "Επέκταση λήξης σε {n} χρήστες",
|
||||
"singular": "Επέκταση λήξης σε {n} χρήστη"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Εκτεταμένη λήξη για {n} χρήστη.",
|
||||
"plural": "Εκτεταμένη λήξη για {n} χρήστες."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Απενεργοποίηση {n} χρήστη",
|
||||
"plural": "Απενεργοποίηση {n} χρηστών"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Εκ νέου ενεργοποίηση {n} χρήστη",
|
||||
"plural": "Εκ νέου ενεργοποίηση {n} χρηστών"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "Απενεργοποιήθηκε {n} χρήστης.",
|
||||
"plural": "Απενεργοποιήθηκαν {n} χρήστες."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Εργοποιήθηκε {n} χρήστης.",
|
||||
"plural": "Εργοποιήθηκαν {n} χρήστες."
|
||||
}
|
||||
}
|
||||
}
|
||||
201
lang/admin/en-gb.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
},
|
||||
"quantityStrings": {
|
||||
"deleteUser": {
|
||||
"singular": "Delete User",
|
||||
"plural": "Delete Users"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Deleted {n} user.",
|
||||
"plural": "Deleted {n} users."
|
||||
},
|
||||
"disabledUser": {
|
||||
"plural": "Disabled {n} users.",
|
||||
"singular": "Disabled {n} user."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Extend expiry for {n} user",
|
||||
"plural": "Extend expiry for {n} users"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"plural": "Extended expiry for {n} users.",
|
||||
"singular": "Extended expiry for {n} user."
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Add user",
|
||||
"plural": "Add users"
|
||||
},
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modify Settings for {n} user",
|
||||
"plural": "Modify Settings for {n} users"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"plural": "Delete {n} users",
|
||||
"singular": "Delete {n} user"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Disable {n} user",
|
||||
"plural": "Disable {n} users"
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Enabled {n} user.",
|
||||
"plural": "Enabled {n} users."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Announce to {n} user",
|
||||
"plural": "Announce to {n} users"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Applied settings to {n} user.",
|
||||
"plural": "Applied settings to {n} users."
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Set expiry for {n} user",
|
||||
"plural": "Set expiry for {n} users"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Re-enable {n} user",
|
||||
"plural": "Re-enable {n} users"
|
||||
}
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invites",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Settings",
|
||||
"inviteDays": "Days",
|
||||
"inviteHours": "Hours",
|
||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||
"inviteSendToEmail": "Send to",
|
||||
"apply": "Apply",
|
||||
"updates": "Updates",
|
||||
"variables": "Variables",
|
||||
"preview": "Preview",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
"ombiProfile": "Ombi user profile",
|
||||
"settingsApplyRestartNow": "Apply & restart",
|
||||
"settingsApplied": "Settings applied.",
|
||||
"userProfiles": "User Profiles",
|
||||
"addProfile": "Add Profile",
|
||||
"userProfilesLibraries": "Libraries",
|
||||
"addProfileNameOf": "Profile Name",
|
||||
"inviteDateCreated": "Created",
|
||||
"settingsRestart": "Restart",
|
||||
"inviteMinutes": "Minutes",
|
||||
"inviteNumberOfUses": "Number of uses",
|
||||
"warning": "Warning",
|
||||
"create": "Create",
|
||||
"name": "Name",
|
||||
"conditionals": "Conditionals",
|
||||
"contactThrough": "Contact through:",
|
||||
"select": "Select",
|
||||
"date": "Date",
|
||||
"extendExpiry": "Extend expiry",
|
||||
"sendPWR": "Send Password Reset",
|
||||
"inviteMonths": "Months",
|
||||
"inviteDuration": "Invite Duration",
|
||||
"update": "Update",
|
||||
"user": "User",
|
||||
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
||||
"templates": "Templates",
|
||||
"accessJFA": "Access jfa-go",
|
||||
"message": "Message",
|
||||
"reset": "Reset",
|
||||
"donate": "Donate",
|
||||
"sendPWRSuccessManual": "If the user hasn't received it, press copy to get a link to manually send to them.",
|
||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||
"logs": "Logs",
|
||||
"sendPWRManual": "User {n} has no method of contact, press copy to get a link to send to them.",
|
||||
"sendPWRSuccess": "Password reset link sent.",
|
||||
"customizeMessages": "Customise Messages",
|
||||
"customizeMessagesDescription": "If you don't want to use jfa-go's message templates, you can create your own using Markdown.",
|
||||
"modifySettings": "Modify Settings",
|
||||
"sendDeleteNotificationEmail": "Send notification message",
|
||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||
"settingsRestarting": "Restarting…",
|
||||
"settingsRestartRequired": "Restart needed",
|
||||
"settingsRefreshPage": "Refresh the page in a few seconds.",
|
||||
"settingsRequiredOrRestartMessage": "Note: {n} indicates a required field, {n} indicates changes require a restart.",
|
||||
"settingsSave": "Save",
|
||||
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile include library access rights and homescreen layout.",
|
||||
"addProfileDescription": "Create a Jellyfin user and configure it, then select it below. When this profile is applied to an invite, new users will be created with the settings.",
|
||||
"addProfileStoreHomescreenLayout": "Store homescreen layout",
|
||||
"inviteNoUsersCreated": "None yet!",
|
||||
"inviteUsersCreated": "Created users",
|
||||
"inviteRemainingUses": "Remaining uses",
|
||||
"inviteNoInvites": "None",
|
||||
"inviteExpiresInTime": "Expires in {n}",
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"notifyUserCreation": "On user creation",
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
||||
"findDiscordUser": "Find Discord user",
|
||||
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
|
||||
"matrixHomeServer": "Home server address",
|
||||
"saveAsTemplate": "Save as template",
|
||||
"deleteTemplate": "Delete template",
|
||||
"settingsRestartRequiredDescription": "A restart is necessary to apply some settings you changed. Restart now or later?",
|
||||
"settingsApplyRestartLater": "Apply, restart later",
|
||||
"subject": "Subject",
|
||||
"setExpiry": "Set expiry",
|
||||
"download": "Download",
|
||||
"search": "Search",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"lastActiveTime": "Last Active",
|
||||
"from": "From",
|
||||
"userExpiry": "User Expiry",
|
||||
"aboutProgram": "About",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "New User",
|
||||
"profile": "Profile",
|
||||
"unknown": "Unknown",
|
||||
"label": "Label",
|
||||
"announce": "Announce",
|
||||
"sendPWRValidFor": "The link is valid for 30m.",
|
||||
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.",
|
||||
"userProfilesIsDefault": "Default",
|
||||
"inviteNoProfile": "No Profile",
|
||||
"searchDiscordUser": "Start typing the Discord username to find the user.",
|
||||
"templateEnterName": "Enter a name to save this template.",
|
||||
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General."
|
||||
},
|
||||
"notifications": {
|
||||
"errorSettingsFailed": "Application failed.",
|
||||
"errorLoadSettings": "Failed to load settings.",
|
||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||
"sentAnnouncement": "Announcement sent.",
|
||||
"savedAnnouncement": "Announcement saved.",
|
||||
"setOmbiProfile": "Stored ombi profile.",
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"accountConnected": "Account connected.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
"errorSaveEmail": "Failed to save email.",
|
||||
"errorLoadProfiles": "Failed to load profiles.",
|
||||
"errorCreateProfile": "Failed to create profile {n}",
|
||||
"errorLoadUsers": "Failed to load users.",
|
||||
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
|
||||
"errorUserCreated": "Failed to create user {n}.",
|
||||
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
|
||||
"errorApplyUpdate": "Failed to apply update, try manually.",
|
||||
"errorCheckUpdate": "Failed to check for update.",
|
||||
"noUpdatesAvailable": "No new updates available.",
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
"userCreated": "User {n} created.",
|
||||
"saveEmail": "Email saved.",
|
||||
"createProfile": "Created profile {n}.",
|
||||
"saveSettings": "Settings were saved",
|
||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||
"errorBlankFields": "Fields were left blank",
|
||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||
"errorChangedEmailAddress": "Couldn't change email address of {n}.",
|
||||
"updateAvailable": "A new update is available, check settings."
|
||||
}
|
||||
}
|
||||
216
lang/admin/en-us.json
Normal file
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invites",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Settings",
|
||||
"inviteMonths": "Months",
|
||||
"inviteDays": "Days",
|
||||
"inviteHours": "Hours",
|
||||
"inviteMinutes": "Minutes",
|
||||
"inviteNumberOfUses": "Number of uses",
|
||||
"inviteDuration": "Invite Duration",
|
||||
"warning": "Warning",
|
||||
"inviteInfiniteUsesWarning": "invites with infinite uses can be used abusively",
|
||||
"inviteSendToEmail": "Send to",
|
||||
"create": "Create",
|
||||
"apply": "Apply",
|
||||
"select": "Select",
|
||||
"name": "Name",
|
||||
"date": "Date",
|
||||
"setExpiry": "Set expiry",
|
||||
"updates": "Updates",
|
||||
"update": "Update",
|
||||
"download": "Download",
|
||||
"search": "Search",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"lastActiveTime": "Last Active",
|
||||
"from": "From",
|
||||
"after": "After",
|
||||
"before": "Before",
|
||||
"user": "User",
|
||||
"userExpiry": "User Expiry",
|
||||
"userExpiryDescription": "A specified amount of time after each signup, jfa-go will delete/disable the account. You can change this behaviour in settings.",
|
||||
"aboutProgram": "About",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "New User",
|
||||
"profile": "Profile",
|
||||
"unknown": "Unknown",
|
||||
"label": "Label",
|
||||
"logs": "Logs",
|
||||
"announce": "Announce",
|
||||
"templates": "Templates",
|
||||
"subject": "Subject",
|
||||
"message": "Message",
|
||||
"variables": "Variables",
|
||||
"conditionals": "Conditionals",
|
||||
"preview": "Preview",
|
||||
"reset": "Reset",
|
||||
"donate": "Donate",
|
||||
"unlink": "Unlink Account",
|
||||
"sendPWR": "Send Password Reset",
|
||||
"contactThrough": "Contact through:",
|
||||
"extendExpiry": "Extend expiry",
|
||||
"sendPWRManual": "User {n} has no method of contact, press copy to get a link to send to them.",
|
||||
"sendPWRSuccess": "Password reset link sent.",
|
||||
"sendPWRSuccessManual": "If the user hasn't received it, press copy to get a link to manually send to them.",
|
||||
"sendPWRValidFor": "The link is valid for 30m.",
|
||||
"customizeMessages": "Customize Messages",
|
||||
"customizeMessagesDescription": "If you don't want to use jfa-go's message templates, you can create your own using Markdown.",
|
||||
"markdownSupported": "Markdown is supported.",
|
||||
"modifySettings": "Modify Settings",
|
||||
"modifySettingsDescription": "Apply settings from an existing profile, or source them directly from a user.",
|
||||
"applyHomescreenLayout": "Apply homescreen layout",
|
||||
"sendDeleteNotificationEmail": "Send notification message",
|
||||
"sendDeleteNotifiationExample": "Your account has been deleted.",
|
||||
"settingsRestart": "Restart",
|
||||
"settingsRestarting": "Restarting…",
|
||||
"settingsRestartRequired": "Restart needed",
|
||||
"settingsRestartRequiredDescription": "A restart is necessary to apply some settings you changed. Restart now or later?",
|
||||
"settingsApplyRestartLater": "Apply, restart later",
|
||||
"settingsApplyRestartNow": "Apply & restart",
|
||||
"settingsApplied": "Settings applied.",
|
||||
"settingsRefreshPage": "Refresh the page in a few seconds.",
|
||||
"settingsRequiredOrRestartMessage": "Note: {n} indicates a required field, {n} indicates changes require a restart.",
|
||||
"settingsSave": "Save",
|
||||
"ombiProfile": "Ombi user profile",
|
||||
"ombiUserDefaultsDescription": "Create an Ombi user and configure it, then select it below. It's settings/permissions will be stored and applied to new Ombi users created by jfa-go when this profile is selected.",
|
||||
"userProfiles": "User Profiles",
|
||||
"userProfilesDescription": "Profiles are applied to users when they create an account. A profile includes library access rights and homescreen layout.",
|
||||
"userProfilesIsDefault": "Default",
|
||||
"userProfilesLibraries": "Libraries",
|
||||
"addProfile": "Add Profile",
|
||||
"addProfileDescription": "Create a Jellyfin user and configure it, then select it below. When this profile is applied to an invite, new users will be created with the settings.",
|
||||
"addProfileNameOf": "Profile Name",
|
||||
"addProfileStoreHomescreenLayout": "Store homescreen layout",
|
||||
"inviteNoUsersCreated": "None yet!",
|
||||
"inviteUsersCreated": "Created users",
|
||||
"inviteNoProfile": "No Profile",
|
||||
"inviteDateCreated": "Created",
|
||||
"inviteRemainingUses": "Remaining uses",
|
||||
"inviteNoInvites": "None",
|
||||
"inviteExpiresInTime": "Expires in {n}",
|
||||
"notifyEvent": "Notify on:",
|
||||
"notifyInviteExpiry": "On expiry",
|
||||
"notifyUserCreation": "On user creation",
|
||||
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
||||
"searchDiscordUser": "Start typing the Discord username to find the user.",
|
||||
"findDiscordUser": "Find Discord user",
|
||||
"linkMatrixDescription": "Enter the username and password of the user to use as a bot. Once submitted, the app will restart.",
|
||||
"matrixHomeServer": "Home server address",
|
||||
"saveAsTemplate": "Save as template",
|
||||
"deleteTemplate": "Delete template",
|
||||
"templateEnterName": "Enter a name to save this template.",
|
||||
"accessJFA": "Access jfa-go",
|
||||
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.",
|
||||
"sortingBy": "Sorting By",
|
||||
"filters": "Filters",
|
||||
"clickToRemoveFilter": "Click to remove this filter.",
|
||||
"clearSearch": "Clear search",
|
||||
"actions": "Actions",
|
||||
"searchOptions": "Search Options",
|
||||
"matchText": "Match Text",
|
||||
"jellyfinID": "Jellyfin ID",
|
||||
"userPageLogin": "User Page: Login",
|
||||
"userPagePage": "User Page: Page",
|
||||
"buildTime": "Build Time",
|
||||
"builtBy": "Built By"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
"userCreated": "User {n} created.",
|
||||
"createProfile": "Created profile {n}.",
|
||||
"saveSettings": "Settings were saved",
|
||||
"saveEmail": "Email saved.",
|
||||
"sentAnnouncement": "Announcement sent.",
|
||||
"savedAnnouncement": "Announcement saved.",
|
||||
"setOmbiProfile": "Stored ombi profile.",
|
||||
"updateApplied": "Update applied, please restart.",
|
||||
"updateAppliedRefresh": "Update applied, please refresh.",
|
||||
"telegramVerified": "Telegram account verified.",
|
||||
"accountConnected": "Account connected.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
|
||||
"errorHomescreenAppliedNoSettings": "Homescreen layout was applied, but applying settings may have failed.",
|
||||
"errorSettingsFailed": "Application failed.",
|
||||
"errorSaveEmail": "Failed to save email.",
|
||||
"errorBlankFields": "Fields were left blank",
|
||||
"errorDeleteProfile": "Failed to delete profile {n}",
|
||||
"errorLoadProfiles": "Failed to load profiles.",
|
||||
"errorCreateProfile": "Failed to create profile {n}",
|
||||
"errorSetDefaultProfile": "Failed to set default profile.",
|
||||
"errorLoadUsers": "Failed to load users.",
|
||||
"errorLoadSettings": "Failed to load settings.",
|
||||
"errorSetOmbiProfile": "Failed to store ombi profile.",
|
||||
"errorLoadOmbiUsers": "Failed to load ombi users.",
|
||||
"errorChangedEmailAddress": "Couldn't change email address of {n}.",
|
||||
"errorFailureCheckLogs": "Failed (check console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
|
||||
"errorUserCreated": "Failed to create user {n}.",
|
||||
"errorSendWelcomeEmail": "Failed to send welcome message (check console/logs)",
|
||||
"errorApplyUpdate": "Failed to apply update, try manually.",
|
||||
"errorCheckUpdate": "Failed to check for update.",
|
||||
"updateAvailable": "A new update is available, check settings.",
|
||||
"noUpdatesAvailable": "No new updates available."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modify Settings for {n} user",
|
||||
"plural": "Modify Settings for {n} users"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Delete {n} user",
|
||||
"plural": "Delete {n} users"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Disable {n} user",
|
||||
"plural": "Disable {n} users"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Re-enable {n} user",
|
||||
"plural": "Re-enable {n} users"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Add user",
|
||||
"plural": "Add users"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Delete User",
|
||||
"plural": "Delete Users"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Deleted {n} user.",
|
||||
"plural": "Deleted {n} users."
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "Disabled {n} user.",
|
||||
"plural": "Disabled {n} users."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "Enabled {n} user.",
|
||||
"plural": "Enabled {n} users."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Announce to {n} user",
|
||||
"plural": "Announce to {n} users"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Applied settings to {n} user.",
|
||||
"plural": "Applied settings to {n} users."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Extend expiry for {n} user",
|
||||
"plural": "Extend expiry for {n} users"
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Set expiry for {n} user",
|
||||
"plural": "Set expiry for {n} users"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Extended expiry for {n} user.",
|
||||
"plural": "Extended expiry for {n} users."
|
||||
}
|
||||
}
|
||||
}
|
||||
204
lang/admin/es-es.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Español (ES)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invitaciones",
|
||||
"accounts": "Cuentas",
|
||||
"settings": "Ajustes",
|
||||
"inviteMonths": "Meses",
|
||||
"inviteDays": "Días",
|
||||
"inviteHours": "Horas",
|
||||
"inviteMinutes": "Minutos",
|
||||
"inviteNumberOfUses": "Números de usos",
|
||||
"inviteDuration": "Duración de invitación",
|
||||
"warning": "Advertencia",
|
||||
"inviteInfiniteUsesWarning": "Las invitaciones con usos infinitos se pueden usar de forma abusiva",
|
||||
"inviteSendToEmail": "Enviar a",
|
||||
"create": "Crear",
|
||||
"apply": "Aplicar",
|
||||
"name": "Nombre",
|
||||
"date": "Fecha",
|
||||
"updates": "Actualizaciones",
|
||||
"update": "Actualizar",
|
||||
"download": "Descargar",
|
||||
"search": "Buscar",
|
||||
"advancedSettings": "Ajustes avanzados",
|
||||
"lastActiveTime": "Último activo",
|
||||
"from": "De",
|
||||
"user": "Usuario",
|
||||
"userExpiry": "Caducidad del usuario",
|
||||
"userExpiryDescription": "Una cantidad de tiempo específica después de cada registro, jfa-go eliminará / deshabilitará la cuenta. Puede cambiar este comportamiento en la configuración.",
|
||||
"aboutProgram": "Acerca de",
|
||||
"version": "Versión",
|
||||
"commitNoun": "Revisión",
|
||||
"newUser": "Nuevo usuario",
|
||||
"profile": "Perfil",
|
||||
"unknown": "Desconocido",
|
||||
"label": "Etiqueta",
|
||||
"announce": "Anunciar",
|
||||
"subject": "Asunto del email",
|
||||
"message": "Mensaje",
|
||||
"variables": "Variables",
|
||||
"preview": "Vista previa",
|
||||
"reset": "Reiniciar",
|
||||
"extendExpiry": "Extender el vencimiento",
|
||||
"customizeMessages": "Personalizar mensajes",
|
||||
"customizeMessagesDescription": "Si no desea utilizar las plantillas de mensajes de jfa-go, puede crear las suyas con Markdown.",
|
||||
"markdownSupported": "Se admite Markdown.",
|
||||
"modifySettings": "Modificar configuración",
|
||||
"modifySettingsDescription": "Aplique la configuración de un perfil existente u obténgalos directamente de un usuario.",
|
||||
"applyHomescreenLayout": "Aplicar el diseño de la pantalla de inicio",
|
||||
"sendDeleteNotificationEmail": "Enviar mensaje de notificación",
|
||||
"sendDeleteNotifiationExample": "Tu cuenta ha sido eliminada.",
|
||||
"settingsRestart": "Reiniciar",
|
||||
"settingsRestarting": "Reiniciando…",
|
||||
"settingsRestartRequired": "Reinicio necesario",
|
||||
"settingsRestartRequiredDescription": "Es necesario reiniciar para aplicar algunas configuraciones que cambió. ¿Reiniciar ahora o más tarde?",
|
||||
"settingsApplyRestartLater": "Aplicar, reiniciar más tarde",
|
||||
"settingsApplyRestartNow": "Aplicar y reiniciar",
|
||||
"settingsApplied": "Se aplicó la configuración.",
|
||||
"settingsRefreshPage": "Actualiza la página en unos segundos.",
|
||||
"settingsRequiredOrRestartMessage": "Nota: {n} indica un campo obligatorio, {n} indica que los cambios requieren un reinicio.",
|
||||
"settingsSave": "Guardar",
|
||||
"ombiUserDefaults": "Valores predeterminados de usuario de Ombi",
|
||||
"ombiUserDefaultsDescription": "Cree un usuario Ombi y configúrelo, luego selecciónelo a continuación. Sus configuraciones/permisos se almacenarán y aplicarán a los nuevos usuarios de Ombi creados por jfa-go cuando se seleccione este perfil.",
|
||||
"userProfiles": "Perfiles de usuario",
|
||||
"userProfilesDescription": "Los perfiles se aplican a los usuarios cuando crean una cuenta. Un perfil incluye los derechos de acceso a la biblioteca y el diseño de la pantalla de inicio.",
|
||||
"userProfilesIsDefault": "Por defecto",
|
||||
"userProfilesLibraries": "Bibliotecas",
|
||||
"addProfile": "Agregar Perfil",
|
||||
"addProfileDescription": "Cree un usuario de Jellyfin y configúrelo, luego selecciónelo a continuación. Cuando este perfil se aplica a una invitación, se crearán nuevos usuarios con la configuración.",
|
||||
"addProfileNameOf": "Nombre de perfil",
|
||||
"addProfileStoreHomescreenLayout": "Guardar el diseño de la pantalla de inicio",
|
||||
"inviteNoUsersCreated": "¡Ninguno todavía!",
|
||||
"inviteUsersCreated": "Usuarios creados",
|
||||
"inviteNoProfile": "Sin perfil",
|
||||
"inviteDateCreated": "Creado",
|
||||
"inviteRemainingUses": "Usos restantes",
|
||||
"inviteNoInvites": "Ninguno",
|
||||
"inviteExpiresInTime": "Caduca en {n}",
|
||||
"notifyEvent": "Notificar en:",
|
||||
"notifyInviteExpiry": "Al vencimiento",
|
||||
"notifyUserCreation": "Sobre la creación de usuarios",
|
||||
"conditionals": "Condicionales",
|
||||
"donate": "Donar",
|
||||
"templates": "Plantillas",
|
||||
"contactThrough": "Contactar a través de:",
|
||||
"select": "Seleccionar",
|
||||
"sendPIN": "Pídale al usuario que envíe el PIN a continuación al bot.",
|
||||
"searchDiscordUser": "Comienza a escribir el nombre de usuario de Discord para encontrar al usuario.",
|
||||
"findDiscordUser": "Encontrar usuario de Discord",
|
||||
"linkMatrixDescription": "Ingrese el nombre de usuario y la contraseña del usuario para usar como bot. Una vez enviada, la aplicación se reiniciará.",
|
||||
"matrixHomeServer": "Dirección del servidor de inicio",
|
||||
"saveAsTemplate": "Guardar como plantilla",
|
||||
"deleteTemplate": "Eliminar plantilla",
|
||||
"templateEnterName": "Ingrese un nombre para guardar esta plantilla.",
|
||||
"setExpiry": "Establecer vencimiento",
|
||||
"sendPWR": "Enviar restablecimiento de contraseña",
|
||||
"sendPWRSuccess": "Se envió el enlace para restablecer la contraseña.",
|
||||
"sendPWRSuccessManual": "Si el usuario no lo ha recibido, presione copiar para generar el enlace y enviárselo manualmente.",
|
||||
"sendPWRValidFor": "El enlace es válido por 30m.",
|
||||
"sendPWRManual": "El usuario {n} no tiene ningún método de contacto, presione copiar para generar el enlace para enviarle.",
|
||||
"ombiProfile": "Perfil de usuario de Ombi",
|
||||
"logs": "Registros",
|
||||
"accessJFA": "Acceso",
|
||||
"accessJFASettings": "No se puede cambia, ya que se ha establecido \"Solo administradores\" o \"Permitir a todos\" en Configuración > General."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Se cambió la dirección de correo electrónico de {n}.",
|
||||
"userCreated": "Usuario {n} creado.",
|
||||
"createProfile": "Perfil creado {n}.",
|
||||
"saveSettings": "Se guardaron las configuraciones",
|
||||
"saveEmail": "Correo electrónico guardado.",
|
||||
"sentAnnouncement": "Anuncio enviado.",
|
||||
"setOmbiDefaults": "Valores predeterminados de ombi almacenados.",
|
||||
"updateApplied": "Actualización aplicada, por favor reinicie.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Se aplicó la configuración, pero es posible que no se haya aplicado el diseño de la pantalla de inicio.",
|
||||
"errorHomescreenAppliedNoSettings": "Se aplicó el diseño de la pantalla de inicio, pero es posible que la aplicación de la configuración haya fallado.",
|
||||
"errorSettingsFailed": "La aplicación falló.",
|
||||
"errorSaveEmail": "No se pudo guardar el correo electrónico.",
|
||||
"errorBlankFields": "Los campos se dejaron en blanco",
|
||||
"errorDeleteProfile": "No se pudo borrar el perfil {n}",
|
||||
"errorLoadProfiles": "No se pudieron cargar los perfiles.",
|
||||
"errorCreateProfile": "No se pudo crear el perfil {n}",
|
||||
"errorSetDefaultProfile": "No se pudo establecer el perfil predeterminado.",
|
||||
"errorLoadUsers": "No se pudieron cargar los usuarios.",
|
||||
"errorLoadSettings": "No se pudo cargar la configuración.",
|
||||
"errorSetOmbiDefaults": "No se pudieron almacenar los valores predeterminados de ombi.",
|
||||
"errorLoadOmbiUsers": "No se pudieron cargar los usuarios de Ombi.",
|
||||
"errorChangedEmailAddress": "No se pudo cambiar la dirección de correo electrónico de {n}.",
|
||||
"errorFailureCheckLogs": "Fallido (ver consola/registros)",
|
||||
"errorPartialFailureCheckLogs": "Fallo parcial (ver consola/registros)",
|
||||
"errorUserCreated": "No se pudo crear el usuario {n}.",
|
||||
"errorSendWelcomeEmail": "No se pudo enviar el mensaje de bienvenida (verifique la consola/registros)",
|
||||
"errorApplyUpdate": "No se pudo aplicar la actualización, intente manualmente.",
|
||||
"errorCheckUpdate": "No se pudo comprobar la actualización.",
|
||||
"updateAvailable": "Hay una nueva actualización disponible, verifique la configuración.",
|
||||
"noUpdatesAvailable": "No hay nuevas actualizaciones disponibles.",
|
||||
"updateAppliedRefresh": "Actualización aplicada, por favor actualice.",
|
||||
"accountConnected": "Cuenta vinculada.",
|
||||
"savedAnnouncement": "Anuncio guardado.",
|
||||
"telegramVerified": "Cuenta de Telegram verificada.",
|
||||
"setOmbiProfile": "Perfil de Ombi guardado.",
|
||||
"errorSetOmbiProfile": "No se pudo guardar el perfil de Ombi."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modificar la configuración de {n} usuario",
|
||||
"plural": "Modificar la configuración de {n} usuarios"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Eliminar {n} usuario",
|
||||
"plural": "Eliminar {n} usuarios"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Desactivar {n} usuario",
|
||||
"plural": "Desactivar {n} usuarios"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Reactivar {n} usuario",
|
||||
"plural": "Reactivar {n} usuarios"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Agregar usuario",
|
||||
"plural": "Agregar usuarios"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Borrar usuario",
|
||||
"plural": "Borrar usuarios"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "{n} usuario eliminado.",
|
||||
"plural": "{n} usuarios eliminados."
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} usuario desactivado.",
|
||||
"plural": "{n} usuarios desactivados."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "{n} usuario activado.",
|
||||
"plural": "{n} usuarios activados."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Anunciar a {n} usuario",
|
||||
"plural": "Anunciar a {n} usuarios"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Se aplicó la configuración a {n} usuario.",
|
||||
"plural": "Se aplicó la configuración a {n} usuarios."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Extender la expiración para {n} usuario",
|
||||
"plural": "Extender la expiración para {n} usuarios"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Caducidad extendida para {n} usuario.",
|
||||
"plural": "Caducidad extendida para {n} usuarios."
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Fijar la caducidad del usuario {n}",
|
||||
"plural": "Establecer la caducidad para {n} usuarios"
|
||||
}
|
||||
}
|
||||
}
|
||||
205
lang/admin/fr-fr.json
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Français (FR)",
|
||||
"author": "https://github.com/Killianbe"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Invitations",
|
||||
"accounts": "Comptes",
|
||||
"settings": "Réglages",
|
||||
"inviteMonths": "Mois",
|
||||
"inviteDays": "Jours",
|
||||
"inviteHours": "Heures",
|
||||
"inviteMinutes": "Minutes",
|
||||
"inviteNumberOfUses": "Nombre d'utilisateurs",
|
||||
"warning": "Attention",
|
||||
"inviteInfiniteUsesWarning": "les invitations infinies peuvent être utilisées abusivement",
|
||||
"inviteSendToEmail": "Envoyer à",
|
||||
"create": "Créer",
|
||||
"apply": "Appliquer",
|
||||
"name": "Nom",
|
||||
"date": "Date",
|
||||
"lastActiveTime": "Dernière activité",
|
||||
"from": "De",
|
||||
"user": "Utilisateur",
|
||||
"aboutProgram": "A propos",
|
||||
"version": "Version",
|
||||
"commitNoun": "Commettre",
|
||||
"newUser": "Nouvel utilisateur",
|
||||
"profile": "Profil",
|
||||
"unknown": "Inconnu",
|
||||
"modifySettings": "Modifier les paramètres",
|
||||
"modifySettingsDescription": "Appliquez les paramètres à partir d'un profil existant ou obtenez-les directement auprès d'un utilisateur.",
|
||||
"applyHomescreenLayout": "Appliquer la disposition de l'écran d'accueil",
|
||||
"sendDeleteNotificationEmail": "Envoyer un message de notification",
|
||||
"sendDeleteNotifiationExample": "Votre compte a été supprimé.",
|
||||
"settingsRestartRequired": "Redémarrage nécessaire",
|
||||
"settingsRestartRequiredDescription": "Un redémarrage est nécessaire pour appliquer certains paramètres que vous avez modifiés. Redémarrer maintenant ou plus tard ?",
|
||||
"settingsApplyRestartLater": "Appliquer, redémarrer plus tard",
|
||||
"settingsApplyRestartNow": "Appliquer et redémarrer",
|
||||
"settingsApplied": "Paramètres appliqués.",
|
||||
"settingsRefreshPage": "Actualisez la page dans quelques secondes.",
|
||||
"settingsRequiredOrRestartMessage": "Remarque : {n} indique un champ obligatoire, {n} indique que les modifications nécessitent un redémarrage.",
|
||||
"settingsSave": "Sauver",
|
||||
"ombiUserDefaults": "Paramètres par défaut de l'utilisateur Ombi",
|
||||
"ombiUserDefaultsDescription": "Créez un utilisateur Ombi et configurez-le, puis sélectionnez-le ci-dessous. Ses paramètres/autorisations seront stockés et appliqués aux nouveaux utilisateurs Ombi créés par jfa-go lorsque ce profil est sélectionné.",
|
||||
"userProfiles": "Profils d'utilisateurs",
|
||||
"userProfilesDescription": "Les profils sont appliqués aux utilisateurs lorsqu'ils créent un compte. Un profil inclut les droits d'accès à la bibliothèque et la disposition de l'écran d'accueil.",
|
||||
"userProfilesIsDefault": "Défaut",
|
||||
"userProfilesLibraries": "Bibliothèques",
|
||||
"addProfile": "Ajouter un profil",
|
||||
"addProfileDescription": "Créez un utilisateur Jellyfin et configurez-le, puis sélectionnez-le ci-dessous. Lorsque ce profil est appliqué à une invitation, les nouveaux utilisateurs seront créés avec ces paramètres.",
|
||||
"addProfileNameOf": "Nom de profil",
|
||||
"addProfileStoreHomescreenLayout": "Enregistrer la disposition de l'écran d'accueil",
|
||||
"inviteNoUsersCreated": "Aucun pour l'instant !",
|
||||
"inviteUsersCreated": "Utilisateurs créés",
|
||||
"inviteNoProfile": "Aucun profil",
|
||||
"inviteDateCreated": "Créer",
|
||||
"inviteRemainingUses": "Utilisations restantes",
|
||||
"inviteNoInvites": "Aucune",
|
||||
"inviteExpiresInTime": "Expires dans {n}",
|
||||
"notifyEvent": "Notifier sur :",
|
||||
"notifyInviteExpiry": "À l'expiration",
|
||||
"notifyUserCreation": "à la création de l'utilisateur",
|
||||
"label": "Nom",
|
||||
"settingsRestarting": "Redémarrage…",
|
||||
"settingsRestart": "Redémarrer",
|
||||
"announce": "Annoncer",
|
||||
"subject": "Sujet",
|
||||
"message": "Message",
|
||||
"markdownSupported": "Markdown est pris en charge.",
|
||||
"customizeMessagesDescription": "Si vous ne souhaitez pas utiliser les modèles d'e-mails de jfa-go, vous pouvez créer les vôtres à l'aide de Markdown.",
|
||||
"variables": "Variables",
|
||||
"preview": "Aperçu",
|
||||
"reset": "Réinitialisation",
|
||||
"customizeMessages": "Personnaliser les e-mails",
|
||||
"inviteDuration": "Durée de l'invitation",
|
||||
"advancedSettings": "Paramètres avancés",
|
||||
"userExpiry": "Expiration de l'utilisateur",
|
||||
"updates": "Mises à jour",
|
||||
"update": "Mise à jour",
|
||||
"download": "Téléchargement",
|
||||
"search": "Recherche",
|
||||
"conditionals": "Conditions",
|
||||
"userExpiryDescription": "Un laps de temps spécifié après chaque inscription, jfa-go supprimera / désactivera le compte. Vous pouvez modifier ce comportement dans les paramètres.",
|
||||
"donate": "Faire un don",
|
||||
"extendExpiry": "Prolonger l'expiration",
|
||||
"contactThrough": "Contacté par :",
|
||||
"sendPIN": "Demandez à l'utilisateur d'envoyer le code PIN ci-dessous au bot.",
|
||||
"select": "Sélectionner",
|
||||
"findDiscordUser": "Trouver l'utilisateur Discord",
|
||||
"linkMatrixDescription": "Entrez le nom d'utilisateur et le mot de passe de l'utilisateur pour l’utilisateur comme bot. Une fois soumis, l'application va redémarrer.",
|
||||
"searchDiscordUser": "Commencez à taper le nom d'utilisateur Discord pour trouver l'utilisateur.",
|
||||
"matrixHomeServer": "Adresse du serveur domestique",
|
||||
"saveAsTemplate": "Sauvegarder comme modèle",
|
||||
"templateEnterName": "Entrez un nom pour sauvegarder ce modèle.",
|
||||
"deleteTemplate": "Supprimer le modèle",
|
||||
"templates": "Modèles",
|
||||
"setExpiry": "Paramétrer l'expiration",
|
||||
"sendPWRSuccess": "Réinitialisation du mot de passe envoyée.",
|
||||
"sendPWR": "Envoyer une réinitialisation du mot de passe",
|
||||
"sendPWRValidFor": "Ce lien est valable 30min.",
|
||||
"sendPWRManual": "L'utilisateur {n} n'a pas indiqué de méthode de contact, appuyez sur copier pour recevoir un lien à lui envoyer.",
|
||||
"sendPWRSuccessManual": "Si l'utilisateur ne l'a pas reçu, appuyez sur copier pour recevoir un lien à lui envoyer manuellement.",
|
||||
"ombiProfile": "Profil d'utilisateur Ombi",
|
||||
"logs": "Logs",
|
||||
"accessJFA": "Accès à jfa-go",
|
||||
"accessJFASettings": "Ne peut pas être changé car \"Admin Only\" ou \"Allow All\" a été défini dans Paramètres > Général."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Adresse e-mail modifiée de {n}.",
|
||||
"userCreated": "L'utilisateur {n} a été créé.",
|
||||
"createProfile": "Profil créé {n}.",
|
||||
"saveSettings": "Les paramètres ont été enregistrés",
|
||||
"setOmbiDefaults": "Valeurs par défaut de Ombi.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Les paramètres ont été appliqués, mais l'application de la disposition de l'écran d'accueil a peut-être échoué.",
|
||||
"errorHomescreenAppliedNoSettings": "La disposition de l'écran d'accueil a été appliquée, mais l'application des paramètres a peut-être échoué.",
|
||||
"errorSettingsFailed": "L'application a échoué.",
|
||||
"errorBlankFields": "Les champs sont vides",
|
||||
"errorDeleteProfile": "Échec de la suppression du profil {n}",
|
||||
"errorLoadProfiles": "Échec du chargement des profils.",
|
||||
"errorCreateProfile": "Échec de la création du profil {n}",
|
||||
"errorSetDefaultProfile": "Échec de la définition du profil par défaut.",
|
||||
"errorLoadUsers": "Échec du chargement des utilisateurs.",
|
||||
"errorLoadSettings": "Échec du chargement des paramètres.",
|
||||
"errorSetOmbiDefaults": "Impossible de stocker les valeurs par défaut d'Ombi.",
|
||||
"errorLoadOmbiUsers": "Échec du chargement des utilisateurs Ombi.",
|
||||
"errorChangedEmailAddress": "Impossible de modifier l'adresse e-mail de {n}.",
|
||||
"errorFailureCheckLogs": "Échec (vérifier la console / les journaux)",
|
||||
"errorPartialFailureCheckLogs": "Panne partielle (vérifier la console / les journaux)",
|
||||
"errorUserCreated": "Echec lors de la création de l'utilisateur {n}.",
|
||||
"errorSendWelcomeEmail": "Echec lors de l'envoi du message de bienvenue (vérifier la console/les journaux)",
|
||||
"sentAnnouncement": "Annonce envoyée.",
|
||||
"saveEmail": "Email enregistré.",
|
||||
"errorSaveEmail": "Échec de l'enregistrement de l'e-mail.",
|
||||
"updateApplied": "Mise à jour appliquée, veuillez redémarrer.",
|
||||
"errorApplyUpdate": "Échec de l'application de la mise à jour, essayez manuellement.",
|
||||
"errorCheckUpdate": "Échec de la vérification de la mise à jour.",
|
||||
"updateAvailable": "Une nouvelle mise à jour est disponible, vérifiez les paramètres.",
|
||||
"noUpdatesAvailable": "Aucune nouvelle mise à jour disponible.",
|
||||
"telegramVerified": "Compte Telegram vérifié.",
|
||||
"updateAppliedRefresh": "Mise à jour appliquée, veuillez actualiser.",
|
||||
"accountConnected": "Compte connecté.",
|
||||
"savedAnnouncement": "Annonce enregistrée.",
|
||||
"setOmbiProfile": "Profil ombi enregistré.",
|
||||
"errorSetOmbiProfile": "Echec de la sauvegarde du profil ombi."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modifier les paramètres pour {n} utilisateur",
|
||||
"plural": "Modifier les paramètres pour {n} utilisateurs"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Supprimer {n} utilisateur",
|
||||
"plural": "Supprimer {n} utilisateurs"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Ajouter un utilisateur",
|
||||
"plural": "Ajouter des utilisateurs"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Supprimer l'utilisateur",
|
||||
"plural": "Supprimer les utilisateurs"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Supprimer {n} utilisateur.",
|
||||
"plural": "Supprimer {n} utilisateurs."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Appliquer le paramètre {n} utilisteur.",
|
||||
"plural": "Appliquer les paramètres {n} utilisteurs."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Annonce à {n} utilisateur",
|
||||
"plural": "Annonce à {n} utilisateurs"
|
||||
},
|
||||
"enabledUser": {
|
||||
"plural": "{n} utilisateurs activés.",
|
||||
"singular": "{n} utilisateur activé."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Prolonger l'expiration pour {n} utilisateur",
|
||||
"plural": "Prolonger l'expiration pour {n} utilisateurs"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Expiration prolongée pour {n} utilisateur.",
|
||||
"plural": "Expiration prolongée pour {n} utilisateurs."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Désactiver {n} utilisateur",
|
||||
"plural": "Désactiver {n} utilisateurs"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Ré-activer {n} utilisateur",
|
||||
"plural": "Ré-activer {n} utilisateurs"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} utilisateur désactivé.",
|
||||
"plural": "{n} utilisateurs désactivés."
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Définir l'expiration pour {n} utilisateur",
|
||||
"plural": "Définir l'expiration pour {n} utilisateurs"
|
||||
}
|
||||
}
|
||||
}
|
||||
195
lang/admin/hu-hu.json
Normal file
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Magyar (HU)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Meghívások",
|
||||
"accounts": "Fiókok",
|
||||
"settings": "Beállítások",
|
||||
"inviteMonths": "Hónapok",
|
||||
"inviteDays": "Napok",
|
||||
"inviteHours": "Órák",
|
||||
"inviteMinutes": "Percek",
|
||||
"inviteNumberOfUses": "Felhasználások száma",
|
||||
"inviteDuration": "Meghívás időtartama",
|
||||
"warning": "Figyelmeztetés",
|
||||
"inviteInfiniteUsesWarning": "a végtelen felhasználású meghívókkal visszaélhetnek",
|
||||
"inviteSendToEmail": "Címzett",
|
||||
"create": "Létrehozás",
|
||||
"apply": "Alkalmaz",
|
||||
"select": "Kiválasztás",
|
||||
"name": "Név",
|
||||
"date": "Dátum",
|
||||
"setExpiry": "Lejárat beállítása",
|
||||
"updates": "Frissítések",
|
||||
"update": "Frissítés",
|
||||
"download": "Letöltés",
|
||||
"search": "Keresés",
|
||||
"advancedSettings": "További beállítások",
|
||||
"lastActiveTime": "Utoljára aktív",
|
||||
"from": "Feladó",
|
||||
"user": "Felhasználó",
|
||||
"userExpiry": "Felhasználói lejárat",
|
||||
"userExpiryDescription": "Egy meghatározott idő után minden regisztrációt töröl, vagy felfüggeszt a jfa-go. Ezt a működést megváltoztathatod a beállításokban.",
|
||||
"aboutProgram": "Névjegy",
|
||||
"version": "Verzió",
|
||||
"commitNoun": "Elkövet",
|
||||
"newUser": "Új felhasználó",
|
||||
"profile": "Profil",
|
||||
"unknown": "Ismeretlen",
|
||||
"label": "Címke",
|
||||
"logs": "Naplók",
|
||||
"announce": "Bejelentés",
|
||||
"templates": "Sablonok",
|
||||
"subject": "Téma",
|
||||
"message": "Üzenet",
|
||||
"variables": "Változók",
|
||||
"conditionals": "Feltételek",
|
||||
"preview": "Előnézet",
|
||||
"reset": "Visszaállítás",
|
||||
"donate": "Támogatás",
|
||||
"sendPWR": "Jelszó visszaállítás küldése",
|
||||
"contactThrough": "",
|
||||
"extendExpiry": "",
|
||||
"sendPWRManual": "",
|
||||
"sendPWRSuccess": "",
|
||||
"sendPWRSuccessManual": "",
|
||||
"sendPWRValidFor": "",
|
||||
"customizeMessages": "",
|
||||
"customizeMessagesDescription": "",
|
||||
"markdownSupported": "",
|
||||
"modifySettings": "",
|
||||
"modifySettingsDescription": "",
|
||||
"applyHomescreenLayout": "",
|
||||
"sendDeleteNotificationEmail": "",
|
||||
"sendDeleteNotifiationExample": "",
|
||||
"settingsRestart": "",
|
||||
"settingsRestarting": "",
|
||||
"settingsRestartRequired": "",
|
||||
"settingsRestartRequiredDescription": "",
|
||||
"settingsApplyRestartLater": "",
|
||||
"settingsApplyRestartNow": "",
|
||||
"settingsApplied": "",
|
||||
"settingsRefreshPage": "",
|
||||
"settingsRequiredOrRestartMessage": "",
|
||||
"settingsSave": "",
|
||||
"ombiProfile": "",
|
||||
"ombiUserDefaultsDescription": "",
|
||||
"userProfiles": "",
|
||||
"userProfilesDescription": "",
|
||||
"userProfilesIsDefault": "",
|
||||
"userProfilesLibraries": "",
|
||||
"addProfile": "",
|
||||
"addProfileDescription": "",
|
||||
"addProfileNameOf": "",
|
||||
"addProfileStoreHomescreenLayout": "",
|
||||
"inviteNoUsersCreated": "",
|
||||
"inviteUsersCreated": "",
|
||||
"inviteNoProfile": "",
|
||||
"inviteDateCreated": "",
|
||||
"inviteRemainingUses": "",
|
||||
"inviteNoInvites": "",
|
||||
"inviteExpiresInTime": "",
|
||||
"notifyEvent": "",
|
||||
"notifyInviteExpiry": "",
|
||||
"notifyUserCreation": "",
|
||||
"sendPIN": "",
|
||||
"searchDiscordUser": "",
|
||||
"findDiscordUser": "",
|
||||
"linkMatrixDescription": "",
|
||||
"matrixHomeServer": "",
|
||||
"saveAsTemplate": "",
|
||||
"deleteTemplate": "",
|
||||
"templateEnterName": ""
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "",
|
||||
"userCreated": "",
|
||||
"createProfile": "",
|
||||
"saveSettings": "",
|
||||
"saveEmail": "",
|
||||
"sentAnnouncement": "",
|
||||
"savedAnnouncement": "",
|
||||
"setOmbiProfile": "",
|
||||
"updateApplied": "",
|
||||
"updateAppliedRefresh": "",
|
||||
"telegramVerified": "",
|
||||
"accountConnected": "",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "",
|
||||
"errorHomescreenAppliedNoSettings": "",
|
||||
"errorSettingsFailed": "",
|
||||
"errorSaveEmail": "",
|
||||
"errorBlankFields": "",
|
||||
"errorDeleteProfile": "",
|
||||
"errorLoadProfiles": "",
|
||||
"errorCreateProfile": "",
|
||||
"errorSetDefaultProfile": "",
|
||||
"errorLoadUsers": "",
|
||||
"errorLoadSettings": "",
|
||||
"errorSetOmbiProfile": "",
|
||||
"errorLoadOmbiUsers": "",
|
||||
"errorChangedEmailAddress": "",
|
||||
"errorFailureCheckLogs": "",
|
||||
"errorPartialFailureCheckLogs": "",
|
||||
"errorUserCreated": "",
|
||||
"errorSendWelcomeEmail": "",
|
||||
"errorApplyUpdate": "",
|
||||
"errorCheckUpdate": "",
|
||||
"updateAvailable": "",
|
||||
"noUpdatesAvailable": ""
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
132
lang/admin/id-id.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Bahasa Indonesia (ID)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Undangan",
|
||||
"accounts": "Akun",
|
||||
"settings": "Pengaturan",
|
||||
"inviteDays": "Hari",
|
||||
"inviteHours": "Jam",
|
||||
"inviteMinutes": "Menit",
|
||||
"inviteNumberOfUses": "Jumlah penggunaan",
|
||||
"warning": "Peringatan",
|
||||
"inviteInfiniteUsesWarning": "Undangan dalam jumlah tak terbatas dapat disalahgunakan",
|
||||
"inviteSendToEmail": "Dikirim kepada",
|
||||
"create": "Buat",
|
||||
"apply": "Terapkan",
|
||||
"name": "Nama",
|
||||
"date": "Tanggal",
|
||||
"lastActiveTime": "Terakhir Aktif",
|
||||
"from": "Dari",
|
||||
"user": "Pengguna",
|
||||
"aboutProgram": "Tentang",
|
||||
"version": "Versi",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "Pengguna Baru",
|
||||
"profile": "Profil",
|
||||
"unknown": "Tidak diketahui",
|
||||
"label": "Label",
|
||||
"modifySettings": "Ganti Pengaturan",
|
||||
"modifySettingsDescription": "Terapkan pengaturan dari profil yang ada, atau dapatkan langsung dari pengguna.",
|
||||
"applyHomescreenLayout": "Terapkan tata letak layar beranda",
|
||||
"sendDeleteNotificationEmail": "Kirim email notifikasi",
|
||||
"sendDeleteNotifiationExample": "Akun anda telah dihapus.",
|
||||
"settingsRestart": "Mulai ulang",
|
||||
"settingsRestarting": "Mengulang kembali…",
|
||||
"settingsRestartRequired": "Mulai ulang diperlukan",
|
||||
"settingsRestartRequiredDescription": "Mulai ulang diperlukan untuk menerapkan beberapa pengaturan yang Anda ubah. Mulai ulang sekarang atau nanti?",
|
||||
"settingsApplyRestartLater": "Terapkan, mulai ulang nanti",
|
||||
"settingsApplyRestartNow": "Terapkan & mulai ulang",
|
||||
"settingsApplied": "Pengaturan diterapkan.",
|
||||
"settingsRefreshPage": "Segarkan halaman dalam beberapa detik.",
|
||||
"settingsRequiredOrRestartMessage": "Catatan: {n} harus diisi, {n} mengindikasikan perubahan memerlukan mulai ulang.",
|
||||
"settingsSave": "Simpan",
|
||||
"ombiUserDefaults": "Default pengguna Ombi",
|
||||
"ombiUserDefaultsDescription": "Buat pengguna Ombi dan konfigurasikan, lalu pilih di bawah. Pengaturan / izinnya akan disimpan dan diterapkan ke pengguna Ombi baru yang dibuat oleh jfa-go",
|
||||
"userProfiles": "Profil Pengguna",
|
||||
"userProfilesDescription": "Profil diterapkan ke pengguna saat mereka membuat akun. Profil mencakup hak akses perpustakaan dan tata letak layar utama.",
|
||||
"userProfilesIsDefault": "Default",
|
||||
"userProfilesLibraries": "Pustaka",
|
||||
"addProfile": "Tambahkan Profil",
|
||||
"addProfileDescription": "Buat pengguna Jellyfin dan konfigurasikan, lalu pilih di bawah. Saat profil ini diterapkan ke undangan, pengguna baru akan dibuat dengan pengaturan seperti ini.",
|
||||
"addProfileNameOf": "Nama Profil",
|
||||
"addProfileStoreHomescreenLayout": "Simpan tata letak layar beranda",
|
||||
"inviteNoUsersCreated": "Belum ada!",
|
||||
"inviteUsersCreated": "Pengguna yang telah dibuat",
|
||||
"inviteNoProfile": "Tidak ada profil",
|
||||
"inviteDateCreated": "Dibuat",
|
||||
"inviteRemainingUses": "Penggunaan yang tersisa",
|
||||
"inviteNoInvites": "Tidak ada",
|
||||
"inviteExpiresInTime": "Kadaluarsa dalam {n}",
|
||||
"notifyEvent": "Beritahu pada:",
|
||||
"notifyInviteExpiry": "Saat kadaluarsa",
|
||||
"notifyUserCreation": "Saat pembuatan pengguna",
|
||||
"variables": "Variabel",
|
||||
"preview": "Pratinjau",
|
||||
"reset": "Setel ulang",
|
||||
"customizeMessages": "Sesuaikan Email",
|
||||
"customizeMessagesDescription": "Jika Anda tidak ingin menggunakan templat email jfa-go, Anda dapat membuatnya sendiri menggunakan Markdown.",
|
||||
"announce": "Mengumumkan",
|
||||
"subject": "Subjek Email",
|
||||
"message": "Pesan",
|
||||
"markdownSupported": "Markdown didukung."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Alamat email {n} diubah.",
|
||||
"userCreated": "Pengguna {n} dibuat.",
|
||||
"createProfile": "Membuat profil {n}.",
|
||||
"saveSettings": "Pengaturan telah disimpan",
|
||||
"setOmbiDefaults": "Default ombi tersimpan.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Pengaturan telah diterapkan, tetapi menerapkan tata letak layar utama mungkin gagal.",
|
||||
"errorHomescreenAppliedNoSettings": "Tata letak layar beranda diterapkan, tetapi menerapkan pengaturan mungkin gagal.",
|
||||
"errorSettingsFailed": "Aplikasi gagal.",
|
||||
"errorBlankFields": "Isian dibiarkan kosong",
|
||||
"errorDeleteProfile": "Gagal menghapus profil {n}",
|
||||
"errorLoadProfiles": "Gagal memuat profil.",
|
||||
"errorCreateProfile": "Gagal membuat profil {n}",
|
||||
"errorSetDefaultProfile": "Gagal menyetel profil default.",
|
||||
"errorLoadUsers": "Gagal memuat pengguna.",
|
||||
"errorLoadSettings": "Gagal memuat pengaturan.",
|
||||
"errorSetOmbiDefaults": "Gagal menyimpan default ombi.",
|
||||
"errorLoadOmbiUsers": "Gagal memuat pengguna ombi.",
|
||||
"errorChangedEmailAddress": "Tidak dapat mengubah alamat email {n}.",
|
||||
"errorFailureCheckLogs": "Gagal (periksa konsol / log)",
|
||||
"errorPartialFailureCheckLogs": "Kegagalan sebagian (periksa konsol / log)",
|
||||
"errorUserCreated": "Gagal membuat pengguna {n}.",
|
||||
"errorSendWelcomeEmail": "Gagal mengirim email selamat datang (periksa konsol / log)",
|
||||
"saveEmail": "Email disimpan.",
|
||||
"sentAnnouncement": "Pengumuman dikirim.",
|
||||
"errorSaveEmail": "Gagal menyimpan email."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Ubah Setelan untuk {n} pengguna",
|
||||
"plural": "Ubah Setelan untuk {n} pengguna"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Hapus {n} pengguna",
|
||||
"plural": "Hapus {n} pengguna"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Tambahkan pengguna",
|
||||
"plural": "Tambahkan pengguna"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Hapus pengguna",
|
||||
"plural": "Hapus pengguna"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Menghapus {n} pengguna.",
|
||||
"plural": "Menghapus {n} pengguna."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Pengaturan diterapkan pada {n} pengguna.",
|
||||
"plural": "Pengaturan diterapkan pada {n} pengguna."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Umumkan kepada {n} pengguna",
|
||||
"plural": "Umumkan kepada {n} pengguna"
|
||||
}
|
||||
}
|
||||
}
|
||||
204
lang/admin/nl-nl.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Nederlands (NL)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Uitnodigingen",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Instellingen",
|
||||
"inviteDays": "Dagen",
|
||||
"inviteHours": "Uren",
|
||||
"inviteMinutes": "Minuten",
|
||||
"inviteNumberOfUses": "Aantal keer te gebruiken",
|
||||
"warning": "Waarschuwing",
|
||||
"inviteInfiniteUsesWarning": "ongelimiteerde uitnodigingen kunnen misbruikt worden",
|
||||
"inviteSendToEmail": "Stuur naar",
|
||||
"create": "Aanmaken",
|
||||
"apply": "Toepassen",
|
||||
"name": "Naam",
|
||||
"date": "Datum",
|
||||
"lastActiveTime": "Laatst actief",
|
||||
"from": "Van",
|
||||
"user": "Gebruiker",
|
||||
"aboutProgram": "Over",
|
||||
"version": "Versie",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "Nieuwe gebruiker",
|
||||
"profile": "Profiel",
|
||||
"unknown": "Onbekend",
|
||||
"modifySettings": "Instellingen aanpassen",
|
||||
"modifySettingsDescription": "Pas instellingen van een bestaand profiel toe, of neem ze direct over van een gebruiker.",
|
||||
"applyHomescreenLayout": "Sla startpagina indeling op",
|
||||
"sendDeleteNotificationEmail": "Stuur melding",
|
||||
"sendDeleteNotifiationExample": "Je account is verwijderd.",
|
||||
"settingsRestartRequired": "Herstart nodig",
|
||||
"settingsRestartRequiredDescription": "Er is een herstart nodig om de wijzigingen door te voeren. Herstart nu of later?",
|
||||
"settingsApplyRestartLater": "Sla op, herstart later",
|
||||
"settingsApplyRestartNow": "Sla op & herstart",
|
||||
"settingsApplied": "Wijzigingen doorgevoerd.",
|
||||
"settingsRefreshPage": "Ververs de pagina over enkele seconden.",
|
||||
"settingsRequiredOrRestartMessage": "Opmerking: {n} is een verplicht veld, {n} geeft aan dat na wijzigen een herstart nodig is.",
|
||||
"settingsSave": "Opslaan",
|
||||
"ombiUserDefaults": "Ombi gebruiker standaardinstellingen",
|
||||
"ombiUserDefaultsDescription": "Maak een Ombi gebruiker aan met de gewenste instellingen, en selecteer deze hieronder. Deze instellingen/rechten worden opgeslagen en toegepast voor nieuwe Ombi gebruikers die jfa-go aanmaakt als dit profiel is geselecteerd.",
|
||||
"userProfiles": "Gebruikersprofielen",
|
||||
"userProfilesDescription": "Profielen worden toegepast op gebruikers wanneer ze een account aanmaken. Een profiel bevat rechten voor bibliotheken en indeling van de startpagina.",
|
||||
"userProfilesIsDefault": "Standaard",
|
||||
"userProfilesLibraries": "Bibliotheken",
|
||||
"addProfile": "Voer profiel toe",
|
||||
"addProfileDescription": "Maak een Jellyfin gebruiker aan met de gewenste instellingen en selecteer deze hieronder. Wanneer dit profiel wordt toegepast op een uitnodiging, worden nieuwe gebruikers aangemaakt met deze instellingen.",
|
||||
"addProfileNameOf": "Profielnaam",
|
||||
"addProfileStoreHomescreenLayout": "Sla startpaginaindeling op",
|
||||
"inviteNoUsersCreated": "Nog geen!",
|
||||
"inviteUsersCreated": "Aangemaakte gebruikers",
|
||||
"inviteNoProfile": "Geen profiel",
|
||||
"inviteDateCreated": "Aangemaakt",
|
||||
"inviteRemainingUses": "Resterend aantal keer te gebruiken",
|
||||
"inviteNoInvites": "Geen",
|
||||
"inviteExpiresInTime": "Verloopt over {n}",
|
||||
"notifyEvent": "Meldingen:",
|
||||
"notifyInviteExpiry": "Bij verloop",
|
||||
"notifyUserCreation": "Bij aanmaken gebruiker",
|
||||
"label": "Label",
|
||||
"settingsRestart": "Herstart",
|
||||
"settingsRestarting": "Aan het herstarten…",
|
||||
"announce": "Aankondiging",
|
||||
"markdownSupported": "Markdown wordt ondersteund.",
|
||||
"subject": "Onderwerp",
|
||||
"message": "Bericht",
|
||||
"variables": "Variabelen",
|
||||
"customizeMessagesDescription": "Als je de e-mailsjablonen van jfa-go niet wilt gebruiken, kun je met gebruik van Markdown je eigen aanmaken.",
|
||||
"preview": "Voorbeeld",
|
||||
"reset": "Reset",
|
||||
"customizeMessages": "E-mails aanpassen",
|
||||
"inviteDuration": "Geldigheidsduur uitnodiging",
|
||||
"userExpiryDescription": "Een bepaalde tijd na elke aanmelding, wordt de account verwijderd/uitgeschakeld door jfa-go. Dit kan aangepast worden in de instellingen.",
|
||||
"userExpiry": "Gebruikersverloop",
|
||||
"extendExpiry": "Verleng verloop",
|
||||
"updates": "Updates",
|
||||
"update": "Bijwerken",
|
||||
"download": "Download",
|
||||
"search": "Zoeken",
|
||||
"advancedSettings": "Geavanceerde instellingen",
|
||||
"inviteMonths": "Maanden",
|
||||
"conditionals": "Voorwaarden",
|
||||
"donate": "Doneer",
|
||||
"contactThrough": "Stuur bericht via:",
|
||||
"sendPIN": "Vraag de gebruiker om onderstaande pincode naar de bot te sturen.",
|
||||
"searchDiscordUser": "Begin de Discord gebruikersnaam te typen om de gebruiker te vinden.",
|
||||
"linkMatrixDescription": "Vul de gebruikersnaam en wachtwoord in van de gebruiker om als bot te gebruiken. De app start zodra ze zijn verstuurd.",
|
||||
"select": "Selecteer",
|
||||
"findDiscordUser": "Zoek Discord gebruiker",
|
||||
"matrixHomeServer": "Adres home server",
|
||||
"templates": "Sjablonen",
|
||||
"templateEnterName": "Voer een naam in om dit sjabloon op te slaan.",
|
||||
"saveAsTemplate": "Sla op als sjabloon",
|
||||
"deleteTemplate": "Verwijder sjabloon",
|
||||
"setExpiry": "Stel verloop in",
|
||||
"sendPWRManual": "Gebruiker {n} heeft geen contactmogelijkheden, druk op kopiëren om een link te krijgen die je kunt sturen.",
|
||||
"sendPWRSuccessManual": "Als de gebruiker hem niet heeft ontvangen, druk dan op kopiëren om een link te krijgen die je handmatig kunt sturen.",
|
||||
"sendPWR": "Verstuur wachtwoordreset",
|
||||
"sendPWRSuccess": "Wachtwoordreset-link verstuurd.",
|
||||
"sendPWRValidFor": "De link is 30m geldig.",
|
||||
"ombiProfile": "Ombi gebruikersprofiel",
|
||||
"logs": "Logs",
|
||||
"accessJFA": "Toegang tot jfa-go",
|
||||
"accessJFASettings": "Kan niet worden aangepast, omdat \"Alleen beheerders\" of \"Laat alle Jellyfin-gebruikers inloggen\" is aangevinkt in Instellingen > Algemeen."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
||||
"userCreated": "Gebruiker {n} aangemaakt.",
|
||||
"createProfile": "Profiel {n} aangemaakt.",
|
||||
"saveSettings": "De instellingen zijn opgeslagen",
|
||||
"setOmbiDefaults": "De ombi standaardinstellingen zijn opgeslagen.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "De instellingen zijn toegepast, maar wijzigen van de startpaginaindeling is misschien mislukt.",
|
||||
"errorHomescreenAppliedNoSettings": "Startpaginaindeling toegepast, maar opslaan van instellingen is misschien mislukt.",
|
||||
"errorSettingsFailed": "Opslaan mislukt.",
|
||||
"errorBlankFields": "Velden leeggelaten",
|
||||
"errorDeleteProfile": "Verwijderen van profiel {n} mislukt",
|
||||
"errorLoadProfiles": "Fout bij het laden van profielen.",
|
||||
"errorCreateProfile": "Aanmaken van profile {n} mislukt",
|
||||
"errorSetDefaultProfile": "Fout bij instellen van standaardprofiel.",
|
||||
"errorLoadUsers": "Laden van gebruikers mislukt.",
|
||||
"errorLoadSettings": "Laden van instellingen mislukt.",
|
||||
"errorSetOmbiDefaults": "Opslaan van ombi standaardinstellingen mislukt.",
|
||||
"errorLoadOmbiUsers": "Laden van ombi gebruikers mislukt.",
|
||||
"errorChangedEmailAddress": "Wijzigen van e-mailadres van {n} mislukt.",
|
||||
"errorFailureCheckLogs": "Mislukt (controleer console/logbestanden)",
|
||||
"errorPartialFailureCheckLogs": "Gedeeltelijke fout (controleer console/logbestanden)",
|
||||
"errorSendWelcomeEmail": "Versturen van welkomstbericht is mislukt (zie console/logs)",
|
||||
"errorUserCreated": "Aanmaken van gebruiker {n} is mislukt.",
|
||||
"sentAnnouncement": "Aankondiging verzonden.",
|
||||
"saveEmail": "E-mail opgeslagen.",
|
||||
"errorSaveEmail": "Opslaan van e-mail mislukt.",
|
||||
"updateApplied": "De update is geïnstalleerd, doe alsjeblieft een herstart.",
|
||||
"errorApplyUpdate": "Installatie van update mislukt, probeer handmatig.",
|
||||
"errorCheckUpdate": "Controleren op update mislukt.",
|
||||
"updateAvailable": "Er is een nieuwe update beschikbaar, kijk bij instellingen.",
|
||||
"noUpdatesAvailable": "Geen nieuwe updates beschikbaar.",
|
||||
"telegramVerified": "Telegram-account goedgekeurd.",
|
||||
"updateAppliedRefresh": "Update toegepast, ververs alsjeblieft.",
|
||||
"accountConnected": "Account gekoppeld.",
|
||||
"savedAnnouncement": "Aankondiging opgeslagen.",
|
||||
"setOmbiProfile": "Opgeslagen ombi-profiel.",
|
||||
"errorSetOmbiProfile": "Opslaan van ombi-profiel mislukt."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Wijzig instellingen voor {n} gebruiker",
|
||||
"plural": "Wijzig instellingen voor {n} gebruikers"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Verwijder {n} gebruiker",
|
||||
"plural": "Verwijder {n} gebruikers"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Voeg gebruiker toe",
|
||||
"plural": "Voer gebruikers toe"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Verwijder gebruiker",
|
||||
"plural": "Verwijder gebruikers"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "{n} gebruiker verwijderd.",
|
||||
"plural": "{n} gebruikers verwijderd."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Instellingen toegepast op {n} gebruiker.",
|
||||
"plural": "Instellingen toegepast op {n} gebruikers."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Aankondigen aan {n} gebruikers",
|
||||
"plural": "aankondigen aan {n} gebruikerss"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Verleng verloop voor {n} gebruiker",
|
||||
"plural": "Verleng verloop voor {n} gebruikers"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Verloop uitgesteld voor {n} gebruiker.",
|
||||
"plural": "Verloop uitgesteld voor {n} gebruikers."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Schakel {n} gebruiker uit",
|
||||
"plural": "Schakel {n} gebruikers uit"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Schakel {n} gebruiker opnieuw in",
|
||||
"plural": "Schakel {n} gebruikers opnieuw in"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} gebruiker uitgeschakeld.",
|
||||
"plural": "{n} gebruikers uitgeschakeld."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "{n} gebruiker ingeschakeld.",
|
||||
"plural": "{n} gebruikers ingeschakeld."
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Stel verloop in voor {n} gebruiker",
|
||||
"plural": "Stel verloop in voor {n} gebruikers"
|
||||
}
|
||||
}
|
||||
}
|
||||
201
lang/admin/pl-pl.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Polski (PL)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Zaproszenia",
|
||||
"accounts": "Konta",
|
||||
"settings": "Ustawienia",
|
||||
"inviteMonths": "Miesiące",
|
||||
"inviteDays": "Dni",
|
||||
"inviteHours": "Godziny",
|
||||
"inviteMinutes": "Minuty",
|
||||
"inviteNumberOfUses": "Liczba użyć",
|
||||
"inviteDuration": "Czas trwania zaproszenia",
|
||||
"warning": "Ostrzeżenie",
|
||||
"inviteInfiniteUsesWarning": "",
|
||||
"inviteSendToEmail": "",
|
||||
"create": "",
|
||||
"apply": "",
|
||||
"select": "",
|
||||
"name": "Imię",
|
||||
"date": "Data",
|
||||
"setExpiry": "",
|
||||
"updates": "Aktualizacje",
|
||||
"update": "Aktualizacja",
|
||||
"download": "Pobierz",
|
||||
"search": "Szukaj",
|
||||
"advancedSettings": "Zaawansowane",
|
||||
"lastActiveTime": "Ostatnia aktywność",
|
||||
"from": "Od",
|
||||
"user": "Użytkownik",
|
||||
"userExpiry": "Użytkownik wygasa",
|
||||
"userExpiryDescription": "",
|
||||
"aboutProgram": "O",
|
||||
"version": "Wersja",
|
||||
"commitNoun": "",
|
||||
"newUser": "",
|
||||
"profile": "",
|
||||
"unknown": "",
|
||||
"label": "",
|
||||
"logs": "",
|
||||
"announce": "",
|
||||
"templates": "",
|
||||
"subject": "",
|
||||
"message": "Wiadomość",
|
||||
"variables": "",
|
||||
"conditionals": "",
|
||||
"preview": "",
|
||||
"reset": "Zresetuj",
|
||||
"donate": "",
|
||||
"sendPWR": "",
|
||||
"contactThrough": "",
|
||||
"extendExpiry": "",
|
||||
"sendPWRManual": "",
|
||||
"sendPWRSuccess": "",
|
||||
"sendPWRSuccessManual": "",
|
||||
"sendPWRValidFor": "",
|
||||
"customizeMessages": "",
|
||||
"customizeMessagesDescription": "",
|
||||
"markdownSupported": "",
|
||||
"modifySettings": "Zmień ustawienia",
|
||||
"modifySettingsDescription": "",
|
||||
"applyHomescreenLayout": "",
|
||||
"sendDeleteNotificationEmail": "",
|
||||
"sendDeleteNotifiationExample": "",
|
||||
"settingsRestart": "",
|
||||
"settingsRestarting": "",
|
||||
"settingsRestartRequired": "",
|
||||
"settingsRestartRequiredDescription": "",
|
||||
"settingsApplyRestartLater": "",
|
||||
"settingsApplyRestartNow": "",
|
||||
"settingsApplied": "",
|
||||
"settingsRefreshPage": "",
|
||||
"settingsRequiredOrRestartMessage": "",
|
||||
"settingsSave": "",
|
||||
"ombiProfile": "",
|
||||
"ombiUserDefaultsDescription": "",
|
||||
"userProfiles": "",
|
||||
"userProfilesDescription": "",
|
||||
"userProfilesIsDefault": "",
|
||||
"userProfilesLibraries": "",
|
||||
"addProfile": "Dodaj Profil",
|
||||
"addProfileDescription": "",
|
||||
"addProfileNameOf": "Nazwa profilu",
|
||||
"addProfileStoreHomescreenLayout": "",
|
||||
"inviteNoUsersCreated": "",
|
||||
"inviteUsersCreated": "",
|
||||
"inviteNoProfile": "",
|
||||
"inviteDateCreated": "Utworzone",
|
||||
"inviteRemainingUses": "",
|
||||
"inviteNoInvites": "",
|
||||
"inviteExpiresInTime": "",
|
||||
"notifyEvent": "",
|
||||
"notifyInviteExpiry": "",
|
||||
"notifyUserCreation": "",
|
||||
"sendPIN": "Poproś użytkownika aby wysłał kod PIN przy użyciu bota.",
|
||||
"searchDiscordUser": "",
|
||||
"findDiscordUser": "",
|
||||
"linkMatrixDescription": "",
|
||||
"matrixHomeServer": "",
|
||||
"saveAsTemplate": "",
|
||||
"deleteTemplate": "Usuń szablon",
|
||||
"templateEnterName": "Wprowadź nazwę aby zapisać szablon.",
|
||||
"accessJFA": "",
|
||||
"accessJFASettings": ""
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Zmieniono adres email {n}.",
|
||||
"userCreated": "Użytkownik {n} utworzony.",
|
||||
"createProfile": "Stworzono profil {n}.",
|
||||
"saveSettings": "Ustawienia zostały zapisane",
|
||||
"saveEmail": "Email zapisany.",
|
||||
"sentAnnouncement": "Ogłoszenie wysłane.",
|
||||
"savedAnnouncement": "Ogłoszenie zostało zapisane.",
|
||||
"setOmbiProfile": "Zapisany profil ombi.",
|
||||
"updateApplied": "Aktualizacja zastosowana, uruchom ponownie.",
|
||||
"updateAppliedRefresh": "Aktualizacja zastosowana, odśwież.",
|
||||
"telegramVerified": "Konto telegramu zweryfikowane.",
|
||||
"accountConnected": "Konto połączone.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Zastosowano ustawienia, ale zastosowanie układu ekranu głównego mogło się nie powieść.",
|
||||
"errorHomescreenAppliedNoSettings": "",
|
||||
"errorSettingsFailed": "",
|
||||
"errorSaveEmail": "",
|
||||
"errorBlankFields": "",
|
||||
"errorDeleteProfile": "",
|
||||
"errorLoadProfiles": "",
|
||||
"errorCreateProfile": "",
|
||||
"errorSetDefaultProfile": "",
|
||||
"errorLoadUsers": "",
|
||||
"errorLoadSettings": "",
|
||||
"errorSetOmbiProfile": "",
|
||||
"errorLoadOmbiUsers": "",
|
||||
"errorChangedEmailAddress": "",
|
||||
"errorFailureCheckLogs": "",
|
||||
"errorPartialFailureCheckLogs": "",
|
||||
"errorUserCreated": "",
|
||||
"errorSendWelcomeEmail": "",
|
||||
"errorApplyUpdate": "",
|
||||
"errorCheckUpdate": "",
|
||||
"updateAvailable": "",
|
||||
"noUpdatesAvailable": ""
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
204
lang/admin/pt-br.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Português (BR)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Convites",
|
||||
"accounts": "Contas",
|
||||
"settings": "Configurações",
|
||||
"inviteDays": "Dias",
|
||||
"inviteHours": "Horas",
|
||||
"inviteMinutes": "Minutos",
|
||||
"inviteNumberOfUses": "Números de usuários",
|
||||
"warning": "Aviso",
|
||||
"inviteInfiniteUsesWarning": "convites infinitos podem ser usados de forma abusiva",
|
||||
"inviteSendToEmail": "Enviar para",
|
||||
"create": "Criar",
|
||||
"apply": "Aplicar",
|
||||
"name": "Nome",
|
||||
"date": "Data",
|
||||
"lastActiveTime": "Ativo pela última vez",
|
||||
"from": "De",
|
||||
"user": "Usuário",
|
||||
"aboutProgram": "Sobre",
|
||||
"version": "Versão",
|
||||
"commitNoun": "Commit",
|
||||
"newUser": "Novo Usuário",
|
||||
"profile": "Perfil",
|
||||
"unknown": "Desconhecido",
|
||||
"label": "Rótulo",
|
||||
"modifySettings": "Modificar configurações",
|
||||
"modifySettingsDescription": "Aplique as configurações de um perfil existente ou obtenha-as diretamente de um usuário.",
|
||||
"applyHomescreenLayout": "Aplicar layout na tela inicial",
|
||||
"sendDeleteNotificationEmail": "Enviar mensagem de notificação",
|
||||
"sendDeleteNotifiationExample": "Sua conta foi deletada.",
|
||||
"settingsRestartRequired": "Necessário reiniciar",
|
||||
"settingsRestartRequiredDescription": "É necessário reiniciar para aplicar algumas configurações alteradas. Deseja reiniciar agora ou mais tarde?",
|
||||
"settingsApplyRestartLater": "Aplicar, reiniciar mais tarde",
|
||||
"settingsApplyRestartNow": "Aplicar e reiniciar",
|
||||
"settingsApplied": "Configurações aplicada.",
|
||||
"settingsRefreshPage": "Atualize a página em alguns segundos.",
|
||||
"settingsRequiredOrRestartMessage": "Nota: {n} indica campo obrigatório, {n} indica que as alterações requer um reinício.",
|
||||
"settingsSave": "Salve",
|
||||
"ombiUserDefaults": "Padrões do usuário Ombi",
|
||||
"ombiUserDefaultsDescription": "Crie um usuário Ombi e configure-o, depois selecione-o abaixo. Suas configurações/permissões serão armazenadas e aplicadas a novos usuários Ombi criados pelo jfa-go quando este perfil for selecionado.",
|
||||
"userProfiles": "Perfil de usuário",
|
||||
"userProfilesDescription": "Os perfis são aplicados aos usuários quando eles criam uma conta. Um perfil inclui direitos de acesso à biblioteca e layout da tela inicial.",
|
||||
"userProfilesIsDefault": "Padrão",
|
||||
"userProfilesLibraries": "Bibliotecas",
|
||||
"addProfile": "Adicionar Perfil",
|
||||
"addProfileDescription": "Crie um usuário no Jellyfin, configure e selecione abaixo. Quando este perfil é aplicado a um convite, novos usuários serão criados com as mesma configurações.",
|
||||
"addProfileNameOf": "Nome do perfil",
|
||||
"addProfileStoreHomescreenLayout": "Layout da tela inicial da loja",
|
||||
"inviteNoUsersCreated": "Nenhum ainda!",
|
||||
"inviteUsersCreated": "Usuários criado",
|
||||
"inviteNoProfile": "Sem Perfil",
|
||||
"inviteDateCreated": "Criado",
|
||||
"inviteRemainingUses": "Uso restantes",
|
||||
"inviteNoInvites": "Nenhum",
|
||||
"inviteExpiresInTime": "Expira em {n}",
|
||||
"notifyEvent": "Notificar em:",
|
||||
"notifyInviteExpiry": "No vencimento",
|
||||
"notifyUserCreation": "Na criação do usuário",
|
||||
"settingsRestart": "Reiniciar",
|
||||
"settingsRestarting": "Reiniciando…",
|
||||
"announce": "Anunciar",
|
||||
"subject": "Assunto",
|
||||
"message": "Mensagem",
|
||||
"markdownSupported": "Suporte a Markdown.",
|
||||
"customizeMessagesDescription": "Se não quiser usar os modelos de email do jfa-go, você pode criar o seu próprio usando o Markdown.",
|
||||
"variables": "Variáveis",
|
||||
"preview": "Pre-visualizar",
|
||||
"reset": "Redefinir",
|
||||
"customizeMessages": "Customizar Emails",
|
||||
"userExpiryDescription": "Após um determinado período de tempo de cada inscrição, o jfa-go apagará/desabilitará a conta. Você pode alterar essa opção nas configurações.",
|
||||
"inviteDuration": "Duração do Convite",
|
||||
"userExpiry": "Vencimento do Usuário",
|
||||
"extendExpiry": "Extender o vencimento",
|
||||
"updates": "Atualizações",
|
||||
"update": "Atualizar",
|
||||
"download": "Download",
|
||||
"search": "Procurar",
|
||||
"advancedSettings": "Configurações Avançada",
|
||||
"inviteMonths": "Meses",
|
||||
"conditionals": "Condicionais",
|
||||
"donate": "Doar",
|
||||
"contactThrough": "Contato através:",
|
||||
"sendPIN": "Peça que o usuário envie o PIN abaixo para o bot.",
|
||||
"searchDiscordUser": "Digite o nome de usuário do Discord.",
|
||||
"findDiscordUser": "Encontrar usuário Discord",
|
||||
"linkMatrixDescription": "Digite o nome de usuário e a senha para usar como bot. Depois de enviado, o aplicativo será reiniciado.",
|
||||
"select": "Selecionar",
|
||||
"templates": "Modelos",
|
||||
"matrixHomeServer": "Endereço do servidor local",
|
||||
"saveAsTemplate": "Salvar o modelo",
|
||||
"deleteTemplate": "Deletar modelo",
|
||||
"templateEnterName": "Digite um nome para salvar este modelo.",
|
||||
"ombiProfile": "Ombi perfil de usuário",
|
||||
"setExpiry": "Definir vencimento",
|
||||
"logs": "Histórico",
|
||||
"sendPWRManual": "O usuário {a} não tem método de contato, pressione copiar para obter um link para enviar a ele.",
|
||||
"accessJFA": "Acessar o jfa-go",
|
||||
"sendPWR": "Enviar redefinição de senha",
|
||||
"sendPWRSuccess": "Link de redefinição de senha enviado.",
|
||||
"sendPWRSuccessManual": "Se o usuário não o recebeu, pressione copiar para obter um link para enviar manualmente a ele.",
|
||||
"sendPWRValidFor": "O link é válido por 30m.",
|
||||
"accessJFASettings": "Não pode ser alterado porque \"Só Administrador\" ou \"Permitir todos\" foi definido em Configurações> Geral."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Endereço de e-mail alterado de {n}.",
|
||||
"userCreated": "Usuário {n} criado.",
|
||||
"createProfile": "Perfil {n} criado.",
|
||||
"saveSettings": "As configurações foram salvas",
|
||||
"setOmbiDefaults": "Padrões do ombi armazenados.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "As configurações foram aplicadas, mas a aplicação do layout da tela inicial pode ter falhado.",
|
||||
"errorHomescreenAppliedNoSettings": "O layout da tela inicial foi aplicado, mas a aplicação das configurações pode ter falhado.",
|
||||
"errorSettingsFailed": "Falha na aplicação.",
|
||||
"errorBlankFields": "Os campos foram deixados em branco",
|
||||
"errorDeleteProfile": "Falha ao excluir perfil {n}",
|
||||
"errorLoadProfiles": "Falha ao carregar perfis.",
|
||||
"errorCreateProfile": "Falha ao criar perfil {n}",
|
||||
"errorSetDefaultProfile": "Falha ao definir o perfil padrão.",
|
||||
"errorLoadUsers": "Falha ao carregar usuários.",
|
||||
"errorLoadSettings": "Falha ao carregar as configurações.",
|
||||
"errorSetOmbiDefaults": "Falha em armazenar os padrões ombi.",
|
||||
"errorLoadOmbiUsers": "Falha ao carregar usuários ombi.",
|
||||
"errorChangedEmailAddress": "Não foi possível alterar o endereço de e-mail de {n}.",
|
||||
"errorFailureCheckLogs": "Falha (verificar console/logs)",
|
||||
"errorPartialFailureCheckLogs": "Falha parcial (verificar console/logs)",
|
||||
"errorUserCreated": "Falha ao criar o usuário {n}.",
|
||||
"errorSendWelcomeEmail": "Falha ao enviar mensagem de boas-vindas (verifique console/logs)",
|
||||
"sentAnnouncement": "Comunicado enviado.",
|
||||
"saveEmail": "Email salvo.",
|
||||
"errorSaveEmail": "Falha ao salvar o email.",
|
||||
"updateApplied": "Atualização aplicada, reinicie.",
|
||||
"errorApplyUpdate": "Falha ao aplicar a atualização, tente manualmente.",
|
||||
"updateAvailable": "Uma nova atualização está disponível, verifique as configurações.",
|
||||
"errorCheckUpdate": "Falha ao verificar atualizações.",
|
||||
"noUpdatesAvailable": "Nenhuma atualização disponível.",
|
||||
"telegramVerified": "Conta do Telegram verificada.",
|
||||
"updateAppliedRefresh": "Atualização instalada, atualize.",
|
||||
"accountConnected": "Conta conectada.",
|
||||
"savedAnnouncement": "Anúncio salvo.",
|
||||
"setOmbiProfile": "Perfil ombi armazenado.",
|
||||
"errorSetOmbiProfile": "Falha ao armazenar o perfil ombi."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Modificar configurações para usuário {n}",
|
||||
"plural": "Modificar configurações para usuários {n}"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Excluir usuário {n}",
|
||||
"plural": "Excluir usuários {n}"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Adicionar usuário",
|
||||
"plural": "Adicionar usuários"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Deletar Usuário",
|
||||
"plural": "Deletar Usuários"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "Excluiu usuário {n}.",
|
||||
"plural": "Excluiu usuários {n}."
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Configurações aplicada ao usuário {n}.",
|
||||
"plural": "Configurações aplicada ao usuários {n}."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Comunicar o usuário {n}",
|
||||
"plural": "Comunicar os usuários {n}"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "Extender o vencimento para {n}",
|
||||
"plural": "Extender o vencimento para {n} usuários"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"plural": "Extender o vencimento para {n} usuários.",
|
||||
"singular": "Extender vencimento para {n}."
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "Desativar {n} usuário",
|
||||
"plural": "Desativar {n} usuários"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "Reativar {n} usuário",
|
||||
"plural": "Reativar {n} usuários"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "{n} Usuário desativado.",
|
||||
"plural": "{n} usuários desativado."
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "{n} Usuário habilitado.",
|
||||
"plural": "{n} Usuários habilitado."
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "Definir expiração para {a} usuário",
|
||||
"plural": "Definir expiração para {a} usuários"
|
||||
}
|
||||
}
|
||||
}
|
||||
144
lang/admin/sv-se.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Svenska (SV)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Inbjudningar",
|
||||
"accounts": "Konton",
|
||||
"settings": "Inställningar",
|
||||
"inviteDays": "Dagar",
|
||||
"inviteHours": "Timmar",
|
||||
"inviteMinutes": "Minuter",
|
||||
"inviteNumberOfUses": "Antal användningar",
|
||||
"warning": "Varning",
|
||||
"inviteInfiniteUsesWarning": "inbjudningar med oändligt antal användningar kan missbrukas",
|
||||
"inviteSendToEmail": "Skicka till",
|
||||
"create": "Skapa",
|
||||
"apply": "Tillämpa",
|
||||
"name": "Namn",
|
||||
"date": "Datum",
|
||||
"lastActiveTime": "Senast aktiv",
|
||||
"from": "Från",
|
||||
"user": "Användare",
|
||||
"aboutProgram": "Om",
|
||||
"version": "Version",
|
||||
"commitNoun": "Skicka",
|
||||
"newUser": "Ny användare",
|
||||
"profile": "Profil",
|
||||
"unknown": "Okänd",
|
||||
"label": "Etikett",
|
||||
"announce": "Meddela",
|
||||
"subject": "E-postämne",
|
||||
"message": "Meddelande",
|
||||
"variables": "Variabler",
|
||||
"preview": "Förhandsvisning",
|
||||
"reset": "Återställ",
|
||||
"customizeMessages": "Anpassa e-post",
|
||||
"customizeMessagesDescription": "Om du inte vill använda jfa-go's e-postmallar, så kan du skapa dina egna med Markdown.",
|
||||
"markdownSupported": "Markdown stöds.",
|
||||
"modifySettings": "Ändra inställningar",
|
||||
"modifySettingsDescription": "Tillämpa inställningar från en befintlig profil eller kopiera dem direkt från en användare.",
|
||||
"applyHomescreenLayout": "Tillämpa hemskärmslayout",
|
||||
"sendDeleteNotificationEmail": "Skicka notifikations e-postmeddelande",
|
||||
"sendDeleteNotifiationExample": "Ditt konto har raderats.",
|
||||
"settingsRestart": "Omstart",
|
||||
"settingsRestarting": "Startar om…",
|
||||
"settingsRestartRequired": "Omstart krävs",
|
||||
"settingsRestartRequiredDescription": "En omstart är nödvändig för att tillämpa vissa inställningar du ändrat. Starta om nu eller senare?",
|
||||
"settingsApplyRestartLater": "Tillämpa, men starta om senare",
|
||||
"settingsApplyRestartNow": "Tillämpa och starta om",
|
||||
"settingsApplied": "Inställningarna tillämpade.",
|
||||
"settingsRefreshPage": "Uppdatera sidan om några sekunder.",
|
||||
"settingsRequiredOrRestartMessage": "Notera: {n} anger ett obligatoriskt fält, {n} anger att ändringar kräver omstart.",
|
||||
"settingsSave": "Spara",
|
||||
"ombiUserDefaults": "Ombi standardanvändare",
|
||||
"ombiUserDefaultsDescription": "Skapa en Ombi-användare och konfigurera denna, välj sedan denna här nedan. Inställningar/behörigheter lagras och tillämpas på nya Ombi-användare som skapats av jfa-go",
|
||||
"userProfiles": "Användarprofiler",
|
||||
"userProfilesDescription": "Profiler tillämpas på användare när de skapar ett konto. En profil inkluderar biblioteksåtkomsträttigheter och layout på hemskärmen.",
|
||||
"userProfilesIsDefault": "Standard",
|
||||
"userProfilesLibraries": "Bibliotek",
|
||||
"addProfile": "Lägg till profil",
|
||||
"addProfileDescription": "Skapa en Jellyfin-användare och konfigurera samt välj denna här nedan. När den här profilen tillämpas på en inbjudan skapas nya användare med inställningarna.",
|
||||
"addProfileNameOf": "Profilnamn",
|
||||
"addProfileStoreHomescreenLayout": "Lagra hemskärmslayout",
|
||||
"inviteNoUsersCreated": "Ingen än!",
|
||||
"inviteUsersCreated": "Skapade användare",
|
||||
"inviteNoProfile": "Ingen profil",
|
||||
"inviteDateCreated": "Skapad",
|
||||
"inviteRemainingUses": "Återstående användningar",
|
||||
"inviteNoInvites": "Ingen",
|
||||
"inviteExpiresInTime": "Går ut om {n}",
|
||||
"notifyEvent": "Meddela den:",
|
||||
"notifyInviteExpiry": "Vid utgång",
|
||||
"notifyUserCreation": "Vid användarskapande",
|
||||
"inviteDuration": "Varaktighet för inbjudan",
|
||||
"userExpiry": "Användarutgång",
|
||||
"userExpiryDescription": "Efter en angiven tid efter varje registrering så tar jfa-go bort/inaktiverar kontot. Du kan ändra detta beteende i inställningarna.",
|
||||
"extendExpiry": "Förläng utgång"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Ändrad e-postadress för {n}.",
|
||||
"userCreated": "Användaren {n} har skapats.",
|
||||
"createProfile": "Skapad profil {n}.",
|
||||
"saveSettings": "Inställningar sparades",
|
||||
"saveEmail": "E-post sparad.",
|
||||
"sentAnnouncement": "Meddelande skickat.",
|
||||
"setOmbiDefaults": "Lagrade ombi-standardvärden.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Inställningarna tillämpades, men tillämpningen av hemskärmslayout kan ha misslyckats.",
|
||||
"errorHomescreenAppliedNoSettings": "Hemskärmslayout tillämpades, men tillämpningen av inställningar kan ha misslyckats.",
|
||||
"errorSettingsFailed": "Tillämpning misslyckades.",
|
||||
"errorSaveEmail": "Det gick inte att spara e-postmeddelandet.",
|
||||
"errorBlankFields": "Fält lämnades tomma",
|
||||
"errorDeleteProfile": "Det gick inte att ta bort profilen {n}",
|
||||
"errorLoadProfiles": "Det gick inte att läsa in profiler.",
|
||||
"errorCreateProfile": "Det gick inte att skapa profilen {n}",
|
||||
"errorSetDefaultProfile": "Det gick inte att ange standardprofil.",
|
||||
"errorLoadUsers": "Det gick inte att läsa in användare.",
|
||||
"errorLoadSettings": "Det gick inte att läsa in inställningarna.",
|
||||
"errorSetOmbiDefaults": "Det gick inte att lagra ombi-standardvärden.",
|
||||
"errorLoadOmbiUsers": "Det gick inte att ladda ombi-användare.",
|
||||
"errorChangedEmailAddress": "Det gick inte att ändra e-postadressen för {n}.",
|
||||
"errorFailureCheckLogs": "Misslyckades (kontrollera konsol/loggar)",
|
||||
"errorPartialFailureCheckLogs": "Partiellt fel (kontrollera konsol/loggarna)",
|
||||
"errorUserCreated": "Det gick inte att skapa användaren {n}.",
|
||||
"errorSendWelcomeEmail": "Det gick inte att skicka välkomstmail (kontrollera konsol/loggar)"
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "Ändra inställningar för {n} användare",
|
||||
"plural": "Ändra inställningar för {n} användare"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "Ta bort {n} användare",
|
||||
"plural": "Ta bort {n} användare"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "Lägg till användare",
|
||||
"plural": "Lägg till användare"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "Radera användare",
|
||||
"plural": "Radera användare"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "{N} användare borttagen.",
|
||||
"plural": "{N} användare har tagits bort."
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "Meddela till {n} användare",
|
||||
"plural": "Meddela till {n} användare"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "Tillämpade inställningar för {n} användare.",
|
||||
"plural": "Tillämpade inställningar för {n} användare."
|
||||
},
|
||||
"extendExpiry": {
|
||||
"plural": "Förläng utgången för {n} användare",
|
||||
"singular": "Förläng utgången för {n} användare"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "Utökad giltighetstid för {n} användare.",
|
||||
"plural": "Utökad giltighetstid för {n} användare."
|
||||
}
|
||||
}
|
||||
}
|
||||
196
lang/admin/vi-vn.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Tiếng Anh (Mỹ)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "Lời mời",
|
||||
"accounts": "Tài khoản",
|
||||
"settings": "Cài đặt",
|
||||
"inviteMonths": "Tháng",
|
||||
"inviteDays": "Ngày",
|
||||
"inviteHours": "Giờ",
|
||||
"inviteMinutes": "Phút",
|
||||
"inviteNumberOfUses": "Số lần sử dụng",
|
||||
"inviteDuration": "Thời hạn hiệu lực",
|
||||
"warning": "Cảnh báo",
|
||||
"inviteInfiniteUsesWarning": "các lời mời không giới hạn số lần sử dụng có thể bị lạm dụng",
|
||||
"inviteSendToEmail": "Gửi tới",
|
||||
"create": "Tạo mới",
|
||||
"apply": "Áp dụng",
|
||||
"select": "Chọn",
|
||||
"name": "Tên",
|
||||
"date": "Ngày",
|
||||
"setExpiry": "Đặt hết hạn",
|
||||
"updates": "Cập nhật",
|
||||
"update": "Cập nhật",
|
||||
"download": "Tải về",
|
||||
"search": "Tìm kiếm",
|
||||
"advancedSettings": "Cài đặt Cấp cao",
|
||||
"lastActiveTime": "Lần cuối Hoạt động",
|
||||
"from": "Từ",
|
||||
"user": "Người dùng",
|
||||
"userExpiry": "Hết hạn Người dùng",
|
||||
"userExpiryDescription": "Sau một khoảng thời gian nhất định sau khi mỗi đăng ký, jfa-go sẽ xóa/vô hiệu hóa tài khoản. Bạn có thể chỉnh sửa chế độ này trong cài đặt.",
|
||||
"aboutProgram": "Thông tin",
|
||||
"version": "Phiên bản",
|
||||
"commitNoun": "Gửi",
|
||||
"newUser": "Người dùng mới",
|
||||
"profile": "Hồ sơ",
|
||||
"unknown": "Không xác định",
|
||||
"label": "Nhãn",
|
||||
"announce": "Thông báo",
|
||||
"templates": "Mẫu",
|
||||
"subject": "Chủ đề",
|
||||
"message": "Tin nhắn",
|
||||
"variables": "Biến",
|
||||
"conditionals": "Điều kiện",
|
||||
"preview": "Xem trước",
|
||||
"reset": "Đặt lại",
|
||||
"donate": "Đóng góp",
|
||||
"sendPWR": "Gửi Đặt lại Mật khẩu",
|
||||
"contactThrough": "Liên lạc qua:",
|
||||
"extendExpiry": "Gia hạn",
|
||||
"sendPWRManual": "Người dùng {n} không có phương thức liên lạc, nhấn chép để lấy đường link để gửi cho họ.",
|
||||
"sendPWRSuccess": "Link đặt lại mật khẩu đã được gửi đi.",
|
||||
"sendPWRSuccessManual": "Nếu người dùng chưa nhận được, nhấn chép để lấy đường link có thể gửi đến họ.",
|
||||
"sendPWRValidFor": "Link có hiệu lực trong vòng 30 phút.",
|
||||
"customizeMessages": "Tùy chỉnh Tin nhắn",
|
||||
"customizeMessagesDescription": "Nếu bạn không muốn sử dụng mẫu tin nhắn của jfa-go, bạn có thể tự tạo mẫu của mình bằng Markdown.",
|
||||
"markdownSupported": "Có hỗ trợ Markdown.",
|
||||
"modifySettings": "Chỉnh sửa Cài đặt",
|
||||
"modifySettingsDescription": "Áp dụng các cài đặt từ một mẫu có sẵn, hoặc lấy trực tiếp từ một người dùng.",
|
||||
"applyHomescreenLayout": "Áp dụng bố cục trang chủ",
|
||||
"sendDeleteNotificationEmail": "Gửi tin nhắn thông báo",
|
||||
"sendDeleteNotifiationExample": "Tài khoản của bạn đã bị xóa.",
|
||||
"settingsRestart": "Khởi động lại",
|
||||
"settingsRestarting": "Đang khởi động lại…",
|
||||
"settingsRestartRequired": "Cần khởi động lại",
|
||||
"settingsRestartRequiredDescription": "Một số cài đặt bạn đã thay đổi cần phải khởi động lại để có hiệu lực. Khởi động lại ngay hay để sau?",
|
||||
"settingsApplyRestartLater": "Áp dụng, khởi động lại sau",
|
||||
"settingsApplyRestartNow": "Áp dụng & khởi động lại",
|
||||
"settingsApplied": "Cài đặt đã được áp dụng.",
|
||||
"settingsRefreshPage": "Làm mới trang trong một vài giây nữa.",
|
||||
"settingsRequiredOrRestartMessage": "Lưu ý: {n} là những cài đặt cần thiết, {n} là những cài đặt cần khởi động lại nếu chúng được thay đổi.",
|
||||
"settingsSave": "Lưu",
|
||||
"ombiProfile": "Tài khoản người dùng Ombi",
|
||||
"ombiUserDefaultsDescription": "Tạo và cài đặt một tài khoản người dùng Ombi, sau đó chọn nó bên dưới. Các cài đặt/quyền sẽ được lưu lại và được áp dụng cho các tài khoản Ombi được tạo bởi jfa-go khi tài khoản mẫy này được chọn.",
|
||||
"userProfiles": "Thông tin Người dùng",
|
||||
"userProfilesDescription": "Mẫu tài khoản được áp dụng cho người dùng khi họ tạo tài khoản. Mẫu tài khoản bao gồm các quyền truy cập thư viện và bốc cục trang chủ.",
|
||||
"userProfilesIsDefault": "Mặc định",
|
||||
"userProfilesLibraries": "Thư viện",
|
||||
"addProfile": "Thêm Tài khoản Mẫu",
|
||||
"addProfileDescription": "Tạo một tài khoản Jellyfin và cấu hình nó, rồi chọn nó bên dưới. Khi tài khoản mẫu này được áp dụng trong lời mời, các người dùng mới sẽ được tạo với các cài đặt tương tự.",
|
||||
"addProfileNameOf": "Tên Tài khoản mẫu",
|
||||
"addProfileStoreHomescreenLayout": "Lưu bố cục trang chủ",
|
||||
"inviteNoUsersCreated": "Chưa có!",
|
||||
"inviteUsersCreated": "Người dùng đã tạo",
|
||||
"inviteNoProfile": "Không có Tài khoản mẫu",
|
||||
"inviteDateCreated": "Tạo",
|
||||
"inviteRemainingUses": "Số lần sử dụng còn lại",
|
||||
"inviteNoInvites": "Không có",
|
||||
"inviteExpiresInTime": "Hết hạn trong {n}",
|
||||
"notifyEvent": "Thông báo khi:",
|
||||
"notifyInviteExpiry": "Khi hết hạn",
|
||||
"notifyUserCreation": "Khi có người dùng được tạo",
|
||||
"sendPIN": "Yêu cầu người dùng gửi mã PIN bên dưới cho bot.",
|
||||
"searchDiscordUser": "Gõ tên người dùng Discord để tìm người dùng.",
|
||||
"findDiscordUser": "Tìm người dùng Discord",
|
||||
"linkMatrixDescription": "Nhập tên đăng nhập và mật khẩu của người dùng được sử dụng làm bot. Khi hoàn thành, ứng dụng sẽ khởi động lại.",
|
||||
"matrixHomeServer": "Địa chỉ máy chủ",
|
||||
"saveAsTemplate": "Lưu thành mẫu",
|
||||
"deleteTemplate": "Xóa mẫu",
|
||||
"templateEnterName": "Nhập tên mẫu để lưu mẫu này.",
|
||||
"logs": "Nhật ký",
|
||||
"accessJFA": "Truy cập jfa-go"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Đã đổi địa chỉ email của {n}.",
|
||||
"userCreated": "Người dùng {n} đã được tạo.",
|
||||
"createProfile": "Đã tạo tài khoản mẫu {n}.",
|
||||
"saveSettings": "Cài đặt đã được lưu",
|
||||
"saveEmail": "Email đã được lưu.",
|
||||
"sentAnnouncement": "Thông báo đã được gửi.",
|
||||
"savedAnnouncement": "Thông báo đã được lưu.",
|
||||
"setOmbiProfile": "Mẫu tài khoản Ombi đã được lưu trữ.",
|
||||
"updateApplied": "Cập nhật mới đã được áp dụng, vui lòng khởi động lại.",
|
||||
"updateAppliedRefresh": "Cập nhật mới đã được áp dụng, vui lòng làm mới lại trang.",
|
||||
"telegramVerified": "Tài khoản Telegram đã được xác thực.",
|
||||
"accountConnected": "Tài khoản đã được kết nối.",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "Cài đặt đã được áp dụng, nhưng việc áp dụng bố cục màn hình chính có thể không thành công.",
|
||||
"errorHomescreenAppliedNoSettings": "",
|
||||
"errorSettingsFailed": "",
|
||||
"errorSaveEmail": "",
|
||||
"errorBlankFields": "",
|
||||
"errorDeleteProfile": "",
|
||||
"errorLoadProfiles": "",
|
||||
"errorCreateProfile": "",
|
||||
"errorSetDefaultProfile": "",
|
||||
"errorLoadUsers": "",
|
||||
"errorLoadSettings": "",
|
||||
"errorSetOmbiProfile": "",
|
||||
"errorLoadOmbiUsers": "",
|
||||
"errorChangedEmailAddress": "",
|
||||
"errorFailureCheckLogs": "",
|
||||
"errorPartialFailureCheckLogs": "",
|
||||
"errorUserCreated": "",
|
||||
"errorSendWelcomeEmail": "",
|
||||
"errorApplyUpdate": "",
|
||||
"errorCheckUpdate": "",
|
||||
"updateAvailable": "",
|
||||
"noUpdatesAvailable": ""
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "",
|
||||
"plural": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
204
lang/admin/zh-hans.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "简体中文(CN)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "邀请",
|
||||
"accounts": "用户",
|
||||
"settings": "设置",
|
||||
"inviteMonths": "月",
|
||||
"inviteDays": "日",
|
||||
"inviteHours": "小时",
|
||||
"inviteMinutes": "分钟",
|
||||
"inviteNumberOfUses": "使用次数",
|
||||
"inviteDuration": "邀请时长",
|
||||
"warning": "警告",
|
||||
"inviteInfiniteUsesWarning": "无限使用次数的邀请码可能被滥用",
|
||||
"inviteSendToEmail": "发送到",
|
||||
"create": "创建",
|
||||
"apply": "申请",
|
||||
"select": "选择",
|
||||
"name": "名称",
|
||||
"date": "日期",
|
||||
"updates": "更新",
|
||||
"update": "更新",
|
||||
"download": "下载",
|
||||
"search": "搜索",
|
||||
"advancedSettings": "高级设置",
|
||||
"lastActiveTime": "上次活动",
|
||||
"from": "从",
|
||||
"user": "用户",
|
||||
"userExpiry": "用户到期",
|
||||
"userExpiryDescription": "每次注册后的指定时间,jfa-go 将删除/禁用该帐户。您可以在设置中更改此行为。",
|
||||
"aboutProgram": "关于",
|
||||
"version": "版本",
|
||||
"commitNoun": "提交",
|
||||
"newUser": "新用户",
|
||||
"profile": "个人资料",
|
||||
"unknown": "未知",
|
||||
"label": "标签",
|
||||
"announce": "宣布",
|
||||
"templates": "模板",
|
||||
"subject": "主题",
|
||||
"message": "信息",
|
||||
"variables": "变量",
|
||||
"conditionals": "条件性条款",
|
||||
"preview": "预览",
|
||||
"reset": "重设",
|
||||
"donate": "捐助",
|
||||
"contactThrough": "联系方式:",
|
||||
"extendExpiry": "延长有效期",
|
||||
"customizeMessages": "自定义消息",
|
||||
"customizeMessagesDescription": "如果不想使用 jfa-go 的消息模板,可以使用 Markdown 创建自己的消息模板。",
|
||||
"markdownSupported": "已支持Markdown。",
|
||||
"modifySettings": "修改设置",
|
||||
"modifySettingsDescription": "应用现有配置文件中的设置,或直接从用户处获取设置。",
|
||||
"applyHomescreenLayout": "应用主屏幕布局",
|
||||
"sendDeleteNotificationEmail": "发送通知消息",
|
||||
"sendDeleteNotifiationExample": "您的帐户已被删除。",
|
||||
"settingsRestart": "重启",
|
||||
"settingsRestarting": "正在重启……",
|
||||
"settingsRestartRequired": "需要重启",
|
||||
"settingsRestartRequiredDescription": "需要重新启动才能应用您更改的某些设置。现在重启还是稍后重启?",
|
||||
"settingsApplyRestartLater": "应用,稍后重启",
|
||||
"settingsApplyRestartNow": "应用并重启",
|
||||
"settingsApplied": "已应用设置。",
|
||||
"settingsRefreshPage": "几秒钟后刷新页面。",
|
||||
"settingsRequiredOrRestartMessage": "注意:{n} 表示必填字段,{n} 表示更改需要重新启动。",
|
||||
"settingsSave": "保存",
|
||||
"ombiUserDefaults": "Ombi 用户默认值",
|
||||
"ombiUserDefaultsDescription": "创建并配置 Ombi 用户,然后在下面选择它。它的设置/权限将被存储并应用于由 jfa-go 创建的新 Ombi 用户。",
|
||||
"userProfiles": "用户档案",
|
||||
"userProfilesDescription": "配置文件在用户创建帐户时应用于用户。配置文件包括库访问权限和主屏幕布局。",
|
||||
"userProfilesIsDefault": "默认",
|
||||
"userProfilesLibraries": "库",
|
||||
"addProfile": "添加档案",
|
||||
"addProfileDescription": "创建一个 Jellyfin 用户并配置它,然后在下面选择它。将此配置文件应用于邀请时,将使用这些设置创建新用户。",
|
||||
"addProfileNameOf": "配置文件名称",
|
||||
"addProfileStoreHomescreenLayout": "保存主屏幕布局",
|
||||
"inviteNoUsersCreated": "暂时不!",
|
||||
"inviteUsersCreated": "已创建的用户",
|
||||
"inviteNoProfile": "没有个人资料",
|
||||
"inviteDateCreated": "已创建",
|
||||
"inviteRemainingUses": "剩余使用次数",
|
||||
"inviteNoInvites": "无",
|
||||
"inviteExpiresInTime": "在 {n} 到期",
|
||||
"notifyEvent": "通知:",
|
||||
"notifyInviteExpiry": "在到期时",
|
||||
"notifyUserCreation": "在创建用户时",
|
||||
"sendPIN": "需要用户将下面的 PIN 发送给机器人。",
|
||||
"searchDiscordUser": "开始输入 Discord 用户名以查找用户。",
|
||||
"findDiscordUser": "查找 Discord 用户",
|
||||
"linkMatrixDescription": "输入要用作机器人的用户的用户名和密码。一旦提交,应用程序将重新启动。",
|
||||
"matrixHomeServer": "主服务器地址",
|
||||
"saveAsTemplate": "保存为模板",
|
||||
"deleteTemplate": "删除模板",
|
||||
"templateEnterName": "输入名称以保存此模板。",
|
||||
"sendPWRManual": "用户 {n} 没有联系方式,请按下钮复制能发给用户的链接。",
|
||||
"sendPWRSuccess": "密码重置链接已发了。",
|
||||
"sendPWR": "发送密码重置",
|
||||
"sendPWRSuccessManual": "如果用户没收到,请按下钮复制链接,手动发给用户。",
|
||||
"setExpiry": "设置到期",
|
||||
"logs": "记录",
|
||||
"sendPWRValidFor": "此链接有效30分钟。",
|
||||
"ombiProfile": "Ombi 用户配置文件",
|
||||
"accessJFASettings": "无法更改,因为“仅限管理员”或“允许所有”已在“设置”>“常规”中设置。",
|
||||
"accessJFA": "访问jfa-go"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "更改了 {n} 的电子邮件地址。",
|
||||
"userCreated": "用户 {n} 已创建。",
|
||||
"createProfile": "创建了配置文件{n}。",
|
||||
"saveSettings": "设置已保存",
|
||||
"saveEmail": "电子邮件已保存。",
|
||||
"sentAnnouncement": "公告已发出。",
|
||||
"savedAnnouncement": "公告已保存。",
|
||||
"setOmbiDefaults": "存储的ombi默认值。",
|
||||
"updateApplied": "已应用更新,请重新启动。",
|
||||
"updateAppliedRefresh": "已应用更新,请刷新。",
|
||||
"telegramVerified": "Telegram账户已验证。",
|
||||
"accountConnected": "帐户已连接。",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "已应用设置,但应用主屏幕布局可能失败。",
|
||||
"errorHomescreenAppliedNoSettings": "已应用主屏幕布局,但应用设置可能失败。",
|
||||
"errorSettingsFailed": "应用失败。",
|
||||
"errorSaveEmail": "电子邮箱保存失败。",
|
||||
"errorBlankFields": "字段留空",
|
||||
"errorDeleteProfile": "删除配置文件{n}失败",
|
||||
"errorLoadProfiles": "加载配置文件失败。",
|
||||
"errorCreateProfile": "创建配置文件{n}失败",
|
||||
"errorSetDefaultProfile": "设置默认配置文件失败。",
|
||||
"errorLoadUsers": "加载用户列表失败。",
|
||||
"errorLoadSettings": "加载配置列表失败。",
|
||||
"errorSetOmbiDefaults": "存储Ombi默认值失败。",
|
||||
"errorLoadOmbiUsers": "加载ombi用户列表失败。",
|
||||
"errorChangedEmailAddress": "无法更改 {n} 的电子邮件地址。",
|
||||
"errorFailureCheckLogs": "失败(检查控制台/日志)",
|
||||
"errorPartialFailureCheckLogs": "部分失败(检查控制台/日志)",
|
||||
"errorUserCreated": "创建用户{n}失败。",
|
||||
"errorSendWelcomeEmail": "发送欢迎消息失败(检查控制台/日志)",
|
||||
"errorApplyUpdate": "无法应用更新,请手动尝试。",
|
||||
"errorCheckUpdate": "检查更新失败。",
|
||||
"updateAvailable": "有新更新可用,请检查设置。",
|
||||
"noUpdatesAvailable": "没有可用的更新。",
|
||||
"setOmbiProfile": "保存ombi配置文件。",
|
||||
"errorSetOmbiProfile": "无法保存ombi配置文件。"
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "修改{n}用户的设置",
|
||||
"plural": "修改{n}用户列表的设置"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "删除 {n} 个用户",
|
||||
"plural": "删除 {n} 个用户"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "禁用 {n} 个用户",
|
||||
"plural": "禁用 {n} 个用户"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "重新启用 {n} 个用户",
|
||||
"plural": "重新启用 {n} 个用户"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "添加用户",
|
||||
"plural": "添加用户"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "删除用户",
|
||||
"plural": "删除用户"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "删除了 {n} 个用户。",
|
||||
"plural": "删除了 {n} 个用户。"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "禁用 了{n} 个用户。",
|
||||
"plural": "禁用 了{n} 个用户。"
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "启用了{n} 个用户。",
|
||||
"plural": "启用了{n} 个用户。"
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "通知 {n} 位用户",
|
||||
"plural": "通知 {n} 位用户"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "将设置应用到 {n} 个用户。",
|
||||
"plural": "将设置应用到 {n} 个用户。"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "延长 {n} 个用户的有效期",
|
||||
"plural": "延长 {n} 个用户的有效期"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "延长了 {n} 个用户的有效期。",
|
||||
"plural": "延长了 {n} 个用户的有效期。"
|
||||
},
|
||||
"setExpiry": {
|
||||
"plural": "为{n}用户设置到期时间",
|
||||
"singular": "为{n}用户设置到期时间"
|
||||
}
|
||||
}
|
||||
}
|
||||
201
lang/admin/zh-hant.json
Normal file
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "繁體中文 (TW)"
|
||||
},
|
||||
"strings": {
|
||||
"invites": "邀請",
|
||||
"accounts": "帳戶",
|
||||
"settings": "設置",
|
||||
"inviteMonths": "月",
|
||||
"inviteDays": "日",
|
||||
"inviteHours": "小時",
|
||||
"inviteMinutes": "分鐘",
|
||||
"inviteNumberOfUses": "使用次數",
|
||||
"inviteDuration": "邀請時長",
|
||||
"warning": "警告",
|
||||
"inviteInfiniteUsesWarning": "無限使用次數的邀請碼可能被濫用",
|
||||
"inviteSendToEmail": "發送到",
|
||||
"create": "創建",
|
||||
"apply": "應用",
|
||||
"select": "選擇",
|
||||
"name": "帳戶名稱",
|
||||
"date": "日期",
|
||||
"setExpiry": "設置到期時間",
|
||||
"updates": "更新",
|
||||
"update": "更新",
|
||||
"download": "下載",
|
||||
"search": "搜尋",
|
||||
"advancedSettings": "高級設置",
|
||||
"lastActiveTime": "上次啟用時間",
|
||||
"from": "從",
|
||||
"user": "帳戶",
|
||||
"userExpiry": "帳戶到期",
|
||||
"userExpiryDescription": "每次註冊后指定的時間,jfa-go 將刪除/禁用該帳戶。您可以在設定中更改此行為。",
|
||||
"aboutProgram": "關於",
|
||||
"version": "版本",
|
||||
"commitNoun": "提交",
|
||||
"newUser": "新帳戶",
|
||||
"profile": "帳戶資料",
|
||||
"unknown": "未知",
|
||||
"label": "標籤",
|
||||
"logs": "日誌",
|
||||
"announce": "公告",
|
||||
"templates": "範本",
|
||||
"subject": "主題",
|
||||
"message": "訊息",
|
||||
"variables": "變數",
|
||||
"conditionals": "條件",
|
||||
"preview": "預覽",
|
||||
"reset": "重設",
|
||||
"donate": "捐贈",
|
||||
"sendPWR": "發送密碼重置",
|
||||
"contactThrough": "聯繫方式:",
|
||||
"extendExpiry": "延長到期時間",
|
||||
"sendPWRManual": "使用者 {n} 沒有聯繫方式,請按 “複製” 以獲取連結並手動發送給使用者。",
|
||||
"sendPWRSuccess": "已發送密碼重置連結。",
|
||||
"sendPWRSuccessManual": "如果使用者沒收到,請按 “複製” 以獲取連結並手動發送給使用者。",
|
||||
"sendPWRValidFor": "該連結的有效期為 30 分鐘。",
|
||||
"customizeMessages": "自定義訊息",
|
||||
"customizeMessagesDescription": "如果您不想使用 jfa-go 的訊息範本,可以使用 Markdown 創建自己的訊息範本。",
|
||||
"markdownSupported": "支持 Markdown。",
|
||||
"modifySettings": "修改設置",
|
||||
"modifySettingsDescription": "應用現有配置文件中的設置,或直接從帳戶處獲取設置。",
|
||||
"applyHomescreenLayout": "應用主螢幕佈局",
|
||||
"sendDeleteNotificationEmail": "發送通知訊息",
|
||||
"sendDeleteNotifiationExample": "您的帳戶已被刪除。",
|
||||
"settingsRestart": "重新啟動",
|
||||
"settingsRestarting": "正在重新啟動…",
|
||||
"settingsRestartRequired": "需要重新啟動",
|
||||
"settingsRestartRequiredDescription": "需要重新啟動才能應用您更改的某些設定。 現在重新啟動還是稍後重新啟動?",
|
||||
"settingsApplyRestartLater": "應用,稍後重新啟動",
|
||||
"settingsApplyRestartNow": "應用並重新啟動",
|
||||
"settingsApplied": "已應用設置。",
|
||||
"settingsRefreshPage": "幾秒鐘後刷新頁面。",
|
||||
"settingsRequiredOrRestartMessage": "注意: {n} 表示必填欄位, {n} 表示更改需要重新啟動。",
|
||||
"settingsSave": "儲存",
|
||||
"ombiProfile": "Ombi 帳戶資料",
|
||||
"ombiUserDefaultsDescription": "創建一個 Ombi 帳戶並對其進行配置,然後在下面選擇它。選擇此設定時,它的設置/權限將被存儲並應用於由 jfa-go 創建的新 Ombi 帳戶。",
|
||||
"userProfiles": "帳戶資料",
|
||||
"userProfilesDescription": "配置文件在使用者創建帳戶時應用於使用者。配置文件包括庫訪問權限和主螢幕佈局。",
|
||||
"userProfilesIsDefault": "預設",
|
||||
"userProfilesLibraries": "庫",
|
||||
"addProfile": "添加配置文件",
|
||||
"addProfileDescription": "創建一個 Jellyfin 帳戶並對其進行配置,然後在下面選擇它。將此設定文件應用於邀請時,將使用這些設置創建新帳戶。",
|
||||
"addProfileNameOf": "配置文件名稱",
|
||||
"addProfileStoreHomescreenLayout": "保存主螢幕佈局",
|
||||
"inviteNoUsersCreated": "暫無!",
|
||||
"inviteUsersCreated": "創建的帳戶",
|
||||
"inviteNoProfile": "無資料",
|
||||
"inviteDateCreated": "已創建",
|
||||
"inviteRemainingUses": "剩餘使用次數",
|
||||
"inviteNoInvites": "無",
|
||||
"inviteExpiresInTime": "在 {n} 到期",
|
||||
"notifyEvent": "通知:",
|
||||
"notifyInviteExpiry": "在到期時",
|
||||
"notifyUserCreation": "在創建用戶時",
|
||||
"sendPIN": "要求使用者將下面的 PIN 發送給機器人。",
|
||||
"searchDiscordUser": "開始鍵入 Discord 帳戶名稱以查找帳戶。",
|
||||
"findDiscordUser": "查尋 Discord 帳戶",
|
||||
"linkMatrixDescription": "輸入要用作機器人的帳戶的帳戶名稱和密碼。提交后,應用程序將重新啟動。",
|
||||
"matrixHomeServer": "主伺服器位址",
|
||||
"saveAsTemplate": "儲存為範本",
|
||||
"deleteTemplate": "刪除範本",
|
||||
"templateEnterName": "輸入名稱以儲存此範本。",
|
||||
"accessJFA": "訪問 jfa-go",
|
||||
"accessJFASettings": "無法更改,因為已在「一般設置」中設置了「僅限管理員帳戶」或「允許全部帳戶」。"
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "更改了 {n} 的電子郵件地址。",
|
||||
"userCreated": "帳戶 {n} 已創建。",
|
||||
"createProfile": "創建了配置文件 {n}。",
|
||||
"saveSettings": "設置已儲存",
|
||||
"saveEmail": "電子郵件已儲存。",
|
||||
"sentAnnouncement": "已發送公告。",
|
||||
"savedAnnouncement": "公告已儲存。",
|
||||
"setOmbiProfile": "ombi 設定已存儲。",
|
||||
"updateApplied": "已應用更新,請重新啟動。",
|
||||
"updateAppliedRefresh": "更新已應用,請重新整理。",
|
||||
"telegramVerified": "Telegram 帳戶已驗證。",
|
||||
"accountConnected": "帳戶已連接。",
|
||||
"errorSettingsAppliedNoHomescreenLayout": "已應用設置,但應用主螢幕佈局可能失敗。",
|
||||
"errorHomescreenAppliedNoSettings": "已應用主螢幕佈局,但應用設置可能失敗。",
|
||||
"errorSettingsFailed": "應用失敗。",
|
||||
"errorSaveEmail": "無法儲存電子郵件。",
|
||||
"errorBlankFields": "欄位留空",
|
||||
"errorDeleteProfile": "無法刪除設置文件 {n}",
|
||||
"errorLoadProfiles": "無法讀取設置文件。",
|
||||
"errorCreateProfile": "無法創建設置文件 {n}",
|
||||
"errorSetDefaultProfile": "無法設置預設設置文件。",
|
||||
"errorLoadUsers": "無法讀取帳戶。",
|
||||
"errorLoadSettings": "無法讀取設置。",
|
||||
"errorSetOmbiProfile": "無法儲存 ombi 設置文件。",
|
||||
"errorLoadOmbiUsers": "無法讀取 ombi 帳戶。",
|
||||
"errorChangedEmailAddress": "無法更改 {n} 的電子郵件地址。",
|
||||
"errorFailureCheckLogs": "失敗(請檢查主控台/紀錄)",
|
||||
"errorPartialFailureCheckLogs": "部分故障(請檢查主控台/日誌)",
|
||||
"errorUserCreated": "無法創建帳戶 {n}。",
|
||||
"errorSendWelcomeEmail": "無法送出歡迎訊息(請檢查主控台/紀錄)",
|
||||
"errorApplyUpdate": "無法應用更新,請手動嘗試。",
|
||||
"errorCheckUpdate": "無法檢查更新。",
|
||||
"updateAvailable": "有新的更新可用,請檢查設置。",
|
||||
"noUpdatesAvailable": "沒有新的更新可用。"
|
||||
},
|
||||
"quantityStrings": {
|
||||
"modifySettingsFor": {
|
||||
"singular": "修改 {n} 個帳戶的設置",
|
||||
"plural": "修改 {n} 個帳戶的設置"
|
||||
},
|
||||
"deleteNUsers": {
|
||||
"singular": "刪除 {n} 個帳戶",
|
||||
"plural": "刪除 {n} 個帳戶"
|
||||
},
|
||||
"disableUsers": {
|
||||
"singular": "禁用 {n} 個帳戶",
|
||||
"plural": "禁用 {n} 個帳戶"
|
||||
},
|
||||
"reEnableUsers": {
|
||||
"singular": "重新啟用 {n} 個帳戶",
|
||||
"plural": "重新啟用 {n} 個帳戶"
|
||||
},
|
||||
"addUser": {
|
||||
"singular": "添加帳戶",
|
||||
"plural": "添加帳戶"
|
||||
},
|
||||
"deleteUser": {
|
||||
"singular": "刪除帳戶",
|
||||
"plural": "刪除帳戶"
|
||||
},
|
||||
"deletedUser": {
|
||||
"singular": "刪除 {n} 個帳戶。",
|
||||
"plural": "刪除 {n} 個帳戶。"
|
||||
},
|
||||
"disabledUser": {
|
||||
"singular": "禁用 {n} 個帳戶。",
|
||||
"plural": "禁用 {n} 個帳戶。"
|
||||
},
|
||||
"enabledUser": {
|
||||
"singular": "已啟用 {n} 個帳戶。",
|
||||
"plural": "已啟用 {n} 個帳戶。"
|
||||
},
|
||||
"announceTo": {
|
||||
"singular": "公告給 {n} 個帳戶",
|
||||
"plural": "公告給 {n} 個帳戶"
|
||||
},
|
||||
"appliedSettings": {
|
||||
"singular": "將設置應用到 {n} 個帳戶。",
|
||||
"plural": "將設置應用到 {n} 個帳戶。"
|
||||
},
|
||||
"extendExpiry": {
|
||||
"singular": "延長 {n} 個帳戶的到期時間",
|
||||
"plural": "延長 {n} 個帳戶的到期時間"
|
||||
},
|
||||
"setExpiry": {
|
||||
"singular": "設置 {n} 個帳戶的到期時間",
|
||||
"plural": "設置 {n} 個帳戶的到期時間"
|
||||
},
|
||||
"extendedExpiry": {
|
||||
"singular": "已延長 {n} 個帳戶的到期時間。",
|
||||
"plural": "已延長 {n} 個帳戶的到期時間。"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
lang/common/ar-aa.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "العربية (AR)"
|
||||
},
|
||||
"strings": {},
|
||||
"notifications": {},
|
||||
"quantityStrings": {}
|
||||
}
|
||||
48
lang/common/da-dk.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Dansk (DK)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Brugernavn",
|
||||
"password": "Adgangskode",
|
||||
"emailAddress": "E-mail Adresse",
|
||||
"name": "Navn",
|
||||
"submit": "Indsend",
|
||||
"send": "Send",
|
||||
"success": "Succes",
|
||||
"continue": "Fortsæt",
|
||||
"error": "Fejl",
|
||||
"copy": "Kopiér",
|
||||
"copied": "Kopiret",
|
||||
"time24h": "24 timers tid",
|
||||
"time12h": "12 timers tid",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Kontakt gennem E-mail",
|
||||
"contactTelegram": "Kontakt gennem Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Kontakt gennem Discord",
|
||||
"theme": "Tema",
|
||||
"refresh": "Opdater",
|
||||
"required": "Påkrævet",
|
||||
"login": "Log på",
|
||||
"logout": "Log ud",
|
||||
"admin": "Administrator",
|
||||
"enabled": "Aktiveret",
|
||||
"disabled": "Deaktiveret",
|
||||
"reEnable": "Genaktiver",
|
||||
"disable": "Deaktiver",
|
||||
"expiry": "Udløb",
|
||||
"add": "Tilføj",
|
||||
"edit": "Rediger",
|
||||
"delete": "Slet"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "Brugernavnet og/eller adgangskoden blev efterladt tomme.",
|
||||
"errorConnection": "Kunne ikke oprette forbindelse til jfa-go.",
|
||||
"errorUnknown": "Ukendt fejl.",
|
||||
"error401Unauthorized": "Adgang nægtet. Prøv at genindlæse siden.",
|
||||
"errorSaveSettings": "Kunne ikke gemme indstillingerne."
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
||||
48
lang/common/de-de.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Deutsch (DE)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort",
|
||||
"emailAddress": "E-Mail Adresse",
|
||||
"name": "Name",
|
||||
"submit": "Absenden",
|
||||
"send": "Senden",
|
||||
"success": "Erfolgreich",
|
||||
"continue": "Weiter",
|
||||
"error": "Fehler",
|
||||
"copy": "Kopieren",
|
||||
"copied": "Kopiert",
|
||||
"time24h": "24h-Format",
|
||||
"time12h": "12h-Format",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Kontakt über E-Mail",
|
||||
"contactTelegram": "Kontakt über Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Kontakt über Discord",
|
||||
"theme": "Thema",
|
||||
"refresh": "Aktualisieren",
|
||||
"required": "Erforderlich",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden",
|
||||
"admin": "Admin",
|
||||
"enabled": "Aktiviert",
|
||||
"disabled": "Deaktiviert",
|
||||
"reEnable": "Wieder aktivieren",
|
||||
"disable": "Deaktivieren",
|
||||
"expiry": "Ablaufdatum",
|
||||
"add": "Hinzufügen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "Der Benutzername und/oder das Passwort wurden nicht ausgefüllt.",
|
||||
"errorConnection": "Konnte keine Verbindung zu jfa-go herstellen.",
|
||||
"errorUnknown": "Unbekannter Fehler.",
|
||||
"error401Unauthorized": "Unberechtigt. Versuch, die Seite zu aktualisieren.",
|
||||
"errorSaveSettings": "Einstellungen konnten nicht gespeichert werden."
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
||||
38
lang/common/el-gr.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Ελληνικά (GR)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Όνομα Χρήστη",
|
||||
"password": "Κωδικός",
|
||||
"emailAddress": "Διεύθυνση Email",
|
||||
"name": "Όνομα",
|
||||
"submit": "Καταχώρηση",
|
||||
"success": "Επιτυχία",
|
||||
"continue": "Συνέχεια",
|
||||
"error": "Σφάλμα",
|
||||
"copy": "Αντιγραφή",
|
||||
"copied": "Αντιγράφηκε",
|
||||
"time24h": "24 Ώρες",
|
||||
"time12h": "12 Ώρες",
|
||||
"theme": "Θέμα",
|
||||
"login": "Σύνδεση",
|
||||
"logout": "Αποσύνδεση",
|
||||
"admin": "Διαχειριστής",
|
||||
"enabled": "Ενεργοποιημένο",
|
||||
"disabled": "Απενεργοποιημένο",
|
||||
"reEnable": "Επανα-ενεργοποίηση",
|
||||
"disable": "Απενεργοποίηση",
|
||||
"expiry": "Λήξη",
|
||||
"edit": "Επεξεργασία",
|
||||
"delete": "Διαγραφή"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "Το όνομα χρήστη και/ή ο κωδικός ήταν κενά.",
|
||||
"errorConnection": "Δεν μπόρεσε να συνδεθεί με το jfa-go.",
|
||||
"errorUnknown": "Άγνωστο σφάλμα.",
|
||||
"error401Unauthorized": "Ανεξουσιοδότητος. Προσπαθήστε να κάνετε επαναφόρτωση την σελίδα.",
|
||||
"errorSaveSettings": "Αποτυχία αποθήκευσης ρυθμίσεων."
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
||||
48
lang/common/en-gb.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (GB)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"emailAddress": "Email Address",
|
||||
"name": "Name",
|
||||
"submit": "Submit",
|
||||
"send": "Send",
|
||||
"success": "Success",
|
||||
"continue": "Continue",
|
||||
"error": "Error",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"theme": "Theme",
|
||||
"refresh": "Refresh",
|
||||
"required": "Required",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"admin": "Admin",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"reEnable": "Re-enable",
|
||||
"disable": "Disable",
|
||||
"expiry": "Expiry",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "The username and/or password was left blank.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"error401Unauthorized": "Unauthorised. Try refreshing the page.",
|
||||
"errorSaveSettings": "Couldn't save settings."
|
||||
},
|
||||
"quantityStrings": {}
|
||||
}
|
||||
65
lang/common/en-us.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "English (US)"
|
||||
},
|
||||
"strings": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"emailAddress": "Email Address",
|
||||
"name": "Name",
|
||||
"submit": "Submit",
|
||||
"send": "Send",
|
||||
"success": "Success",
|
||||
"continue": "Continue",
|
||||
"error": "Error",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"time24h": "24h Time",
|
||||
"time12h": "12h Time",
|
||||
"linkTelegram": "Link Telegram",
|
||||
"contactEmail": "Contact through Email",
|
||||
"contactTelegram": "Contact through Telegram",
|
||||
"linkDiscord": "Link Discord",
|
||||
"linkMatrix": "Link Matrix",
|
||||
"contactDiscord": "Contact through Discord",
|
||||
"theme": "Theme",
|
||||
"refresh": "Refresh",
|
||||
"required": "Required",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"admin": "Admin",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"reEnable": "Re-enable",
|
||||
"disable": "Disable",
|
||||
"contactMethods": "Contact Methods",
|
||||
"accountStatus": "Account Status",
|
||||
"notSet": "Not set",
|
||||
"expiry": "Expiry",
|
||||
"add": "Add",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"myAccount": "My Account"
|
||||
},
|
||||
"notifications": {
|
||||
"errorLoginBlank": "The username and/or password were left blank.",
|
||||
"errorConnection": "Couldn't connect to jfa-go.",
|
||||
"errorUnknown": "Unknown error.",
|
||||
"error401Unauthorized": "Unauthorized. Try refreshing the page.",
|
||||
"errorSaveSettings": "Couldn't save settings."
|
||||
},
|
||||
"quantityStrings": {
|
||||
"year": {
|
||||
"singular": "{n} Year",
|
||||
"plural": "{n} Years"
|
||||
},
|
||||
"month": {
|
||||
"singular": "{n} Month",
|
||||
"plural": "{n} Months"
|
||||
},
|
||||
"day": {
|
||||
"singular": "{n} Day",
|
||||
"plural": "{n} Days"
|
||||
}
|
||||
}
|
||||
}
|
||||