mirror of
https://git.joinfirefish.org/firefish/firefish.git
synced 2024-05-19 02:01:12 +02:00
Compare commits
949 commits
Author | SHA1 | Date | |
---|---|---|---|
945465cc5e | |||
fb232b4797 | |||
3661c4da74 | |||
654e71e43a | |||
819cb9a824 | |||
c18987e3b4 | |||
2613dfd952 | |||
dfc8763f0c | |||
3d5003acc2 | |||
b8e5b8616a | |||
3a7c5fcdbb | |||
1c0d2e43b4 | |||
d9ff9101a1 | |||
58268a2c6e | |||
c4a7cd6029 | |||
2a8193fa6c | |||
ada5ff7e75 | |||
7dd3b8ec5a | |||
88890153a1 | |||
23f47aac38 | |||
abc9d58f7c | |||
ab3ca2a20b | |||
fdc77b74ae | |||
61562a0943 | |||
a298302f76 | |||
42d4f2fd79 | |||
234e5b8619 | |||
1904fbaa43 | |||
c1f5bbd2e8 | |||
282944a199 | |||
fcb4017700 | |||
18eaa915b8 | |||
52456a509e | |||
8c9a1abd32 | |||
2ee48321fd | |||
1d1e9105e5 | |||
e9287357f7 | |||
29023d7240 | |||
4e50ffeb09 | |||
2fdd165144 | |||
7e8d71003f | |||
1e313846ca | |||
ffa08748d0 | |||
5e53f9a8cf | |||
a4779f233b | |||
fd5f27eb49 | |||
3d28acb2c9 | |||
ff6ad1c2a9 | |||
20cd64281a | |||
e47f5f0cfc | |||
0ad1c581b0 | |||
ead1db27a5 | |||
12be7266db | |||
7ad4ffecf3 | |||
4e39a85708 | |||
c21bd40a5c | |||
1b537da466 | |||
8cee026c72 | |||
0ee3db7788 | |||
ff63089128 | |||
287dcece57 | |||
9298a6252d | |||
088dfd21e7 | |||
03323e40fa | |||
c6e3506bd5 | |||
128fc72778 | |||
310059f6a0 | |||
7d4d1c1fbd | |||
dbd205972f | |||
41b32c5535 | |||
56be2f034e | |||
e15bcee86c | |||
43326cdf8d | |||
7d1947792d | |||
d28fe77d9f | |||
acc13e9b10 | |||
4e31e11f81 | |||
dddd2779c0 | |||
832fc7cd1d | |||
a18ad132be | |||
4b96063c23 | |||
0de54e02f8 | |||
101e50926b | |||
9cf88f0df6 | |||
efb6cc9132 | |||
58f3eb4924 | |||
5adc0e581d | |||
c0b760cda5 | |||
eb967564f9 | |||
0085105e72 | |||
217b3ecf80 | |||
ffeeb3b444 | |||
2f00947a24 | |||
5608129913 | |||
8923e1f2a7 | |||
8765e6ba54 | |||
7c72738983 | |||
ff446de7e8 | |||
411d00a7af | |||
65a1fa870b | |||
1d25c78866 | |||
6067eaef04 | |||
92299423a3 | |||
65a8984c09 | |||
99eb364778 | |||
266c81df1e | |||
6a2e91efa1 | |||
17cbb9cd1e | |||
d6ebb55556 | |||
4dd1cff80b | |||
752c6dc75b | |||
cede0fdae2 | |||
35d706e45d | |||
9075050a67 | |||
edc2a7d890 | |||
28e2a24585 | |||
2884b2fb42 | |||
d8e1ab63c0 | |||
c2d5859755 | |||
457bd22b7b | |||
6176c09509 | |||
e9068acddd | |||
3ccacb7fce | |||
654ab006a6 | |||
1942d772db | |||
bc08f0faa9 | |||
fa35d1f4dd | |||
4db42272e7 | |||
cfa3263c46 | |||
b09e418cf6 | |||
e5a5d715b6 | |||
9ad61fe607 | |||
d6983e92aa | |||
07e2571c79 | |||
0a9289abe3 | |||
9dda7f955a | |||
a14a4a5f9c | |||
b3eb12ccff | |||
6cd511d473 | |||
a82ed86539 | |||
30ce11f9fe | |||
afb2d30e65 | |||
fb3cd43102 | |||
1cd58ce05d | |||
55fdd8b5a2 | |||
704d39f202 | |||
ef95673d50 | |||
eb443c8494 | |||
52e3b49533 | |||
11ded9491e | |||
d3b899ccc3 | |||
6d6c0fbca0 | |||
2257721fe3 | |||
cd836daa9b | |||
bf5a2c6ebb | |||
f0632d2a6b | |||
d192a7c81a | |||
847cc47fc4 | |||
59862f16b0 | |||
0e5e96c99a | |||
dbc24a0b8f | |||
1eb26263c1 | |||
639a838736 | |||
811be1022a | |||
7f277878a6 | |||
119cbe3e4f | |||
2e15165117 | |||
1b526c651e | |||
afe06edd16 | |||
3bdf4f9f9c | |||
567ba873e3 | |||
6eef39158e | |||
f81739b8d1 | |||
ff7fffc711 | |||
daade3865b | |||
7b24210bd8 | |||
d77da088f8 | |||
d6541a3ebb | |||
4fbf211e96 | |||
132615958b | |||
27c4b4c812 | |||
99f5063f4c | |||
ef706fff9d | |||
84528680df | |||
11b5f5cc17 | |||
da752a158c | |||
693c9edb10 | |||
4110842357 | |||
8656bdb185 | |||
af4426653e | |||
3750ca426b | |||
3a8ca2a2d7 | |||
ace081f163 | |||
184b0e4019 | |||
bec62cffc6 | |||
d5493f8e5d | |||
1cb64b7fa8 | |||
8f59f26aa0 | |||
6f6333f094 | |||
96cbc6799c | |||
d4f1e06535 | |||
f9e2bd2448 | |||
b07dc87af6 | |||
aa266d91e0 | |||
8f8d62aa58 | |||
d1b33ad76f | |||
eeb09028bd | |||
ded0de27c5 | |||
ac57e4c019 | |||
291990d320 | |||
2ca7bd65aa | |||
fb4e449139 | |||
084c7f1c84 | |||
421030c38f | |||
f933525856 | |||
9c8e5eabb4 | |||
4d3072929e | |||
612ce48f44 | |||
95fd20a46f | |||
3886c5624b | |||
fc7de024c6 | |||
bd88c3399f | |||
5b8a164b8d | |||
b3cc8cdb3c | |||
8373623136 | |||
d04f85d4bd | |||
33853a3a9b | |||
26a58c92df | |||
4a81106cf5 | |||
bdc5d02d27 | |||
075d326d7b | |||
0a7f16c11f | |||
3af8f86924 | |||
276cabbbe3 | |||
af14bee31f | |||
b3d1be457b | |||
347851d6bb | |||
abec71074b | |||
272e30be0c | |||
971f196627 | |||
8cc0e40d35 | |||
beeea86253 | |||
084a4bc63a | |||
cda31d3dc7 | |||
907578e8f8 | |||
2923ea86de | |||
226c990385 | |||
769f52c8ee | |||
8a00d82f36 | |||
34ed877f57 | |||
f5074f35cc | |||
a847dd55ad | |||
5382dc5da8 | |||
989e93f2a0 | |||
df81cb6a85 | |||
31168cc7b2 | |||
42886f054d | |||
1d0ea11eea | |||
24602c4745 | |||
33923a59fa | |||
8067ed4084 | |||
4277ad0b59 | |||
fc65d8c1c3 | |||
3b3d457c3e | |||
1128e243d3 | |||
39e08f57e8 | |||
09ef642905 | |||
1b8748bc8c | |||
82c98ae72f | |||
5b3f93457b | |||
4d9c0f8e7b | |||
bf2b624bc9 | |||
5261eb24b6 | |||
d440e9b388 | |||
14b285f882 | |||
baa5c402db | |||
5b01d3574f | |||
e3a98ebc72 | |||
7fe7f90350 | |||
8ed942e00f | |||
ddfdd038ad | |||
7fdd44cf8d | |||
0c4826becf | |||
ecd8e3d109 | |||
a3b156441a | |||
ecbd8a8724 | |||
442dc33a34 | |||
c8372767fa | |||
8e497b41cf | |||
bfdf73caeb | |||
5b18f9761c | |||
641ff742bb | |||
e6121946aa | |||
c6212ff8f4 | |||
d582a84c57 | |||
a7978e2b08 | |||
766bac3dee | |||
7360736966 | |||
e797849e9b | |||
ef57735e6a | |||
e7c33835b2 | |||
4e83dbd01f | |||
dd74eabae1 | |||
711618b42c | |||
510207b101 | |||
49825853c1 | |||
359fef0a42 | |||
fda81a9f91 | |||
c505c6df36 | |||
9a4a75bf92 | |||
5e5d01d407 | |||
d114b8ec1d | |||
d2471b6db7 | |||
341b43ed71 | |||
6d64358674 | |||
4992999bb7 | |||
38c0de39b9 | |||
722d090f8d | |||
b185c0c87e | |||
8c22b0d07f | |||
0f4c05a64f | |||
bc39badf51 | |||
1ee8198c6f | |||
15c32d5510 | |||
bf3f4906ac | |||
dbe10f88b0 | |||
369b1d72df | |||
e6ba0a002f | |||
37e03007f0 | |||
f66ecd0759 | |||
5ab81c2799 | |||
8a2f3b3e36 | |||
f7c576f8fb | |||
46d0679845 | |||
4536ac0678 | |||
160e7f26a6 | |||
4a0e4a4c91 | |||
a8f659ab88 | |||
64c07a2406 | |||
6e9ae06990 | |||
caae8474a6 | |||
9138c3726a | |||
425b333474 | |||
d1c76b3882 | |||
d82f212c24 | |||
f85e6ebb19 | |||
587c64a906 | |||
a4a96f0026 | |||
c0c5cb92cd | |||
fbfbffcb72 | |||
5ad9bd8ceb | |||
c345d80f4a | |||
f40c201670 | |||
c8c7abe6ef | |||
1a46d1394c | |||
32176f86b2 | |||
7d02a8852b | |||
1f8745b268 | |||
Laura Hausmann | e790d6be90 | ||
0b6d0b525f | |||
1e9e5d1c18 | |||
21e0439b5e | |||
74b843614a | |||
956592147c | |||
2bf8eab74a | |||
54b6e14621 | |||
5600799261 | |||
c47d5d70e7 | |||
f14a5bcb25 | |||
4fcd5463e9 | |||
ce0cec216c | |||
0272b08df8 | |||
a44e9f3e38 | |||
941a31c702 | |||
64ca96d759 | |||
fe1efed369 | |||
aed2e9853a | |||
50d662d62b | |||
9c3ba0c593 | |||
c2a9b028a3 | |||
cc560811cf | |||
28b9e35f2a | |||
51f9d20073 | |||
98cc23557f | |||
e5bac649c8 | |||
53dfec57a8 | |||
1b143ebfaa | |||
38cd4bafde | |||
d880601d56 | |||
c89525d151 | |||
2ccc45cfad | |||
aae505ad68 | |||
f80ee9f36d | |||
efcadf681f | |||
cd3c6a52dd | |||
a7a47f7d3b | |||
564eb08386 | |||
dc8ddff007 | |||
54f100032e | |||
98048699c1 | |||
37cf4f8361 | |||
d40db1ee7e | |||
0caba566e6 | |||
883645a581 | |||
3190f66740 | |||
8b73a1a6b3 | |||
41f9a0bda9 | |||
99f30ba01a | |||
bfcadaa094 | |||
18439f6e27 | |||
f806c47c7c | |||
b58d940e71 | |||
42f704b515 | |||
8534154792 | |||
1cfe3bfb73 | |||
ba8e044f42 | |||
79ab7bf787 | |||
23e57737a6 | |||
5c60e030e8 | |||
0512dca83c | |||
1757560f36 | |||
154ae05fb7 | |||
8e5d31b606 | |||
2d15d6dcfa | |||
171e2f3973 | |||
fd40c3dd2e | |||
3b172a7762 | |||
13b648f6bf | |||
320f933e9d | |||
fec1a800b6 | |||
22b52ac3d3 | |||
af109b45ef | |||
ab357233db | |||
6aecf12067 | |||
fefed2d4a4 | |||
bb984a8608 | |||
5de6e07dfa | |||
9a077c4beb | |||
7c827682dd | |||
0b7385e16a | |||
214e999c8c | |||
b12d7e4c63 | |||
27c46d7df9 | |||
b9c3dfbd3d | |||
5891a90f71 | |||
d82ad33730 | |||
cdea55ab25 | |||
76a45fa6ac | |||
d086b7b993 | |||
aa2b35a554 | |||
19b94b2fb8 | |||
e5a21bae13 | |||
adafd62710 | |||
217d686997 | |||
92b1f5cb5f | |||
42f6a6e3b6 | |||
40a4109c76 | |||
24e6e31b2b | |||
7441f0861a | |||
a2316b7caf | |||
322b2392de | |||
bf9ab9c1ca | |||
339cbac191 | |||
bb5349d127 | |||
93e55f146b | |||
eac0c1c47c | |||
44ad4bce32 | |||
861b66c6ad | |||
feafaffd12 | |||
7e9633a36b | |||
e67b3e8ad5 | |||
dbad2a485f | |||
4816d75e23 | |||
9bf17dcb3c | |||
ac9a07d25a | |||
89a8547cb1 | |||
f895e002ea | |||
fbd980aeb8 | |||
71e4621e26 | |||
cc37177bb5 | |||
109884f6d8 | |||
3bed093344 | |||
243adaaa0d | |||
879d499486 | |||
9db729d734 | |||
a2958f6da8 | |||
9ef5350a00 | |||
e8b39be387 | |||
88280e3bfd | |||
ee04e30f25 | |||
267670af96 | |||
ea60895bf8 | |||
5994a1d615 | |||
9eccdba075 | |||
cac438b965 | |||
067810b1be | |||
50b7c71ed6 | |||
8f68693510 | |||
9a42745926 | |||
c0afa4a2f7 | |||
62f5c84ca6 | |||
6a0ad409cd | |||
850201ff71 | |||
6f324a3dcd | |||
ccd26a826f | |||
4bb97f2a3c | |||
93bee484bb | |||
07d39cb5ac | |||
192929f0f4 | |||
be2837551c | |||
d9e46f7fa6 | |||
c6d0fe52d2 | |||
08926ceb8c | |||
a49f9c33ef | |||
1d3b67eafb | |||
29da813170 | |||
c5a344c2a0 | |||
a856523119 | |||
83e3f96ced | |||
a52340fa53 | |||
9acd130a22 | |||
0c1e7cdd72 | |||
cbd15fb2ca | |||
a6498b0491 | |||
9ced0d96ad | |||
9a4988eaad | |||
fbdc068115 | |||
23ec206aee | |||
07444ae7c1 | |||
455ecdf743 | |||
d98c564ead | |||
56aac15a6b | |||
280dddf464 | |||
1347c6ff04 | |||
b3cc01c440 | |||
ebaefb9697 | |||
d9982a0b6a | |||
0cb2e94d99 | |||
78b6228b3c | |||
d1817d9a22 | |||
c9de5f6095 | |||
c4658801aa | |||
a107d8c1ec | |||
4c91e8e37f | |||
8140694a31 | |||
509690d84d | |||
b9e88ce490 | |||
dc53447fa3 | |||
0c9dc92f07 | |||
9fef36e80c | |||
b4ae877462 | |||
2a30b4a536 | |||
3039458c4c | |||
574d3b3fe5 | |||
137d0fe3e5 | |||
5c6f1c818a | |||
2d8f4b945f | |||
77e6479a67 | |||
ec940bb068 | |||
9b91035a79 | |||
ce672f4edd | |||
2fa0ca355d | |||
131b3686d4 | |||
09bcbb0ff0 | |||
cdc3b5181a | |||
70e4d78f90 | |||
1dfc7c443b | |||
3917818c76 | |||
6b008c651a | |||
d2dbfb37c7 | |||
96481f1353 | |||
c936102a4c | |||
43570a54aa | |||
4d34e14dd8 | |||
28f7ac1acd | |||
9f3396af21 | |||
dac4043dd9 | |||
d1e898c0d0 | |||
dc02a07774 | |||
2760e7feee | |||
488323cc8e | |||
3b65ebcb3e | |||
6349705fb2 | |||
4e20fe589d | |||
b08175fb83 | |||
e5e2eedfb4 | |||
a2699e6687 | |||
39a229b875 | |||
35c7dccb49 | |||
77ded03330 | |||
dd3ad89b64 | |||
4fb2cab617 | |||
2b0668dacd | |||
f13df7e202 | |||
f33c7c6c94 | |||
d41b462a89 | |||
ccbd6178e4 | |||
f486caf244 | |||
b017f9ce94 | |||
b157cbe79a | |||
5c4a773ecf | |||
ccb17977c3 | |||
def62ff1ce | |||
56038b174d | |||
037b7950a2 | |||
e82a07c03d | |||
e60898b4b0 | |||
3b89a8bfa6 | |||
207855b0e8 | |||
781c98dda7 | |||
ab221c98a7 | |||
6c46bb56fd | |||
1be5373dfc | |||
968657d26e | |||
913de651db | |||
4aeb0d95cc | |||
c0f93de94b | |||
4823abd3a9 | |||
c6e2776298 | |||
4b7724ed1f | |||
9d87679800 | |||
78092cd4be | |||
ff08d044b5 | |||
bce88ec199 | |||
1b076c96d7 | |||
c19c439ac1 | |||
cc452da6c5 | |||
30969ad817 | |||
8337863ed3 | |||
6a94d1b65d | |||
c471aa30ae | |||
a411f4e4d9 | |||
22f4278ab5 | |||
ec7578e78e | |||
adaaae1583 | |||
8489066130 | |||
17fb05430e | |||
bf3c0717b9 | |||
082948bfe0 | |||
86f2e32c66 | |||
7af2cca2d4 | |||
07384a4f0f | |||
77a2bcfc4b | |||
fd333250c9 | |||
38192052c9 | |||
80b80277e2 | |||
71c158fbd3 | |||
a07483996e | |||
0f3126196f | |||
2731003bc9 | |||
74875f174b | |||
884c69f377 | |||
f412d7ace3 | |||
eb5dccacfe | |||
21225f7137 | |||
fca48b2a81 | |||
b71da18b03 | |||
241c824ab5 | |||
54d9916fec | |||
f0a50bc288 | |||
ceca260c92 | |||
70aa3704ef | |||
baa57d7c17 | |||
19b45866c8 | |||
8a62bf90f5 | |||
cee3a13f51 | |||
aea6659d0b | |||
1a6ba246f2 | |||
16880b1231 | |||
3e43819ba1 | |||
5ba88f3d6f | |||
008d8d8f5c | |||
bb9a58ce34 | |||
ad58ae8f30 | |||
799ad1f3f8 | |||
d57c9dc289 | |||
d0de0d14b2 | |||
baba86a203 | |||
f95e2d1c51 | |||
2b88ef18a5 | |||
a86be6a9cb | |||
68be7d6be9 | |||
5eff4da27b | |||
f44a2937d4 | |||
64581d2088 | |||
0517a83dc5 | |||
393ab2590d | |||
99b8a40566 | |||
603ec70b97 | |||
f422842aef | |||
695cb452bc | |||
e4927c9b9b | |||
35ca75bfe5 | |||
148c3736ce | |||
3d6031dca3 | |||
83c15b1026 | |||
ea6ef881c2 | |||
0cfa85197d | |||
e2cd25ea4f | |||
2bf51abc12 | |||
3e32fc7b04 | |||
08bea415c9 | |||
5da03666b2 | |||
9592b88e52 | |||
8926d86ab8 | |||
6b812fb30b | |||
0335a353e8 | |||
50b2d1c22b | |||
ad30d16f23 | |||
452e0b921c | |||
beb16ab9cf | |||
f3b189a70c | |||
95b91c6396 | |||
9c75dd0b26 | |||
0b90a7daaf | |||
f60ff2eaf4 | |||
186919aac3 | |||
2ba60a3b40 | |||
b6b07d562c | |||
c231bf43cb | |||
5d2238a266 | |||
8b6e300d4e | |||
0491e11a9e | |||
3fe8ace571 | |||
afc57d834c | |||
38668c4c11 | |||
24f79d4796 | |||
e6b7eca775 | |||
6eee5c651c | |||
8e28f0e97c | |||
d8b4eb6f5e | |||
ef94ba1474 | |||
88aaa47220 | |||
69b93554e8 | |||
5f2697f304 | |||
9bbdb1638f | |||
aedf873248 | |||
c4a093209f | |||
db37eb4ad1 | |||
3716e7f74c | |||
7a4e6334f1 | |||
783f5481bb | |||
da6f07952b | |||
3ddd68097a | |||
f275fc9cdf | |||
18ba024cbb | |||
43aeec32ce | |||
909125e519 | |||
124b2244d6 | |||
0b7ab1b90d | |||
6982843716 | |||
17a945b8b1 | |||
f7f7959ba6 | |||
c1155f169a | |||
b59186f093 | |||
1a1d817772 | |||
73537ec6fa | |||
6ac6a4cfa9 | |||
b6baded2e3 | |||
23145c61af | |||
268b7aeb3f | |||
8287874cd6 | |||
4f50156931 | |||
80f8afbb26 | |||
cbaa577e58 | |||
12f04a19b4 | |||
75899aeb51 | |||
6ee1049731 | |||
3bde79e6a9 | |||
8dd0a8b752 | |||
e486715e9a | |||
bd62b5e964 | |||
0ed207d81b | |||
0b231e6d24 | |||
8b14e57ba6 | |||
c707221dfb | |||
e1345e4e29 | |||
3f918f9dcc | |||
273f1a8568 | |||
2beeeddca2 | |||
7adef666d1 | |||
fcfbee4c1d | |||
30619301d3 | |||
fce0e5a218 | |||
2ca4d17697 | |||
086253ebb1 | |||
a75c0c5fd7 | |||
3edeb7a270 | |||
9df08ea16a | |||
15ddde2431 | |||
65e7d9d157 | |||
26c3b25a40 | |||
ea357b71a0 | |||
d0901d77ab | |||
a64e42208e | |||
cb7e9ab449 | |||
110e092262 | |||
9ea5fcd1bf | |||
5e2376a6dc | |||
fb3cc4b4f1 | |||
6a7d8c0b13 | |||
e4c9a91a16 | |||
18f4114f18 | |||
5c0d771dff | |||
6efcddd0ee | |||
d6a88419a4 | |||
e9030a6b82 | |||
aa11eeb1c8 | |||
c52973234c | |||
383b2b874d | |||
91ccce4307 | |||
3c469ccf43 | |||
ba140134bf | |||
ac69480f28 | |||
6dcf8c65b4 | |||
54c5192a06 | |||
ced13ff12b | |||
12aa04447b | |||
0a080159d4 | |||
98fc5af2a8 | |||
8a74892df4 | |||
a401239cfd | |||
0c74ac471e | |||
01158d7dd5 | |||
3251a07ae0 | |||
bfffd94ae5 | |||
3abe6a0dfe | |||
3586d0cde7 | |||
995d679afd | |||
3902b82470 | |||
85e42f7c27 | |||
94a1b01564 | |||
13d607c58d | |||
5560b6d481 | |||
99b928c833 | |||
9bfa8dc417 | |||
75cf048876 | |||
5d3cfcff36 | |||
ee3ce22b74 | |||
2505b58f99 | |||
ae071d66b7 | |||
3f8bd14f0d | |||
66dec9c54e | |||
6b89d666af | |||
435260e197 | |||
93e80353eb | |||
3d39daff8c | |||
914fff5658 | |||
d0989c692d | |||
add6081f18 | |||
ff6e722f0c | |||
7c712df731 | |||
6dc4f3d8a1 | |||
46baec14d2 | |||
acf64acb11 | |||
45a217adaf | |||
82dff9beb1 | |||
97e6dc1bb4 | |||
6f1c29cf05 | |||
84eb75e03e | |||
a5cd3c8b70 | |||
81b9fdf397 | |||
f70c9efe6b | |||
0001ffa356 | |||
76097b25c9 | |||
70dd49740e | |||
e8f178458f | |||
c735156ef2 | |||
f2075e361a | |||
0b226f7013 | |||
7e0f20c24f | |||
12abccc9a3 | |||
cc83abac7e | |||
059f9e1be1 | |||
02aef92d8b | |||
0666a78dcf | |||
a562d9bb39 | |||
9a0136514b | |||
54dfe1c222 | |||
Laura Hausmann | 88ca0e1621 | ||
9086ef11ff | |||
0d990e07cb | |||
3ba7137eaa | |||
cbe88c56ef | |||
6d4cb5b4aa | |||
9b354790c3 | |||
46bf02cdd5 | |||
f346d2e2f6 | |||
2267e90d3b | |||
db0bd21edc | |||
d9ee4737bc | |||
62c46fda65 | |||
a097e1aa98 | |||
4370bd1012 | |||
25a1fa341a | |||
0d92fa303b | |||
f32d4bb764 | |||
6cf3d7f84c | |||
267e26900a | |||
3b2db417a8 | |||
81856c15e6 | |||
0f51768ff7 | |||
b3668f67a0 | |||
Laura Hausmann | e753b313da | ||
Laura Hausmann | 2e51a33ae5 | ||
Laura Hausmann | ada0137a35 | ||
Laura Hausmann | 850c52ef63 | ||
00b15bb17c | |||
ce69001243 | |||
11bbb1cfbd | |||
96a430bf5e | |||
51c3dde159 | |||
5019a2b4b8 | |||
fc213c1d6b | |||
7b7649cbc0 | |||
970b6d7632 | |||
4c83684385 | |||
73ab605188 | |||
fffd0976c9 | |||
293afc7fe4 | |||
611a2f02b9 | |||
284f077dba | |||
a98a26e901 | |||
7e5c1ee0b5 | |||
5284537a7c | |||
e66032bb46 | |||
e55b854b8a | |||
45a20bc63a | |||
d37c375e5d | |||
e9193dc1f5 | |||
bd3be6fe80 | |||
2b9504f386 | |||
9506a3e812 | |||
d9681f9b5b | |||
10b8613449 | |||
6cd5dc2fcb | |||
400f227237 | |||
5e2b8d3be0 | |||
d1d0328f8b | |||
bd5e95a1f0 | |||
bab704992f | |||
9ee7804990 | |||
4e2c7e14bf | |||
d5d09d849b | |||
d64d133d7f | |||
02e6aebd33 | |||
3fb388f9b8 | |||
9e61a6bbe0 | |||
de4da3c1fd |
192
.config/ci.yml
192
.config/ci.yml
|
@ -1,195 +1,11 @@
|
|||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Firefish configuration
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────┐
|
||||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
url: https://example.tld/
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# URL SETTINGS AFTER THAT!
|
||||
|
||||
# ┌───────────────────────┐
|
||||
#───┘ Port and TLS settings └───────────────────────────────────
|
||||
|
||||
#
|
||||
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||
#
|
||||
# +----- https://example.tld/ ------------+
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# +---------------------------------------+
|
||||
#
|
||||
# You need to set up a reverse proxy. (e.g. nginx)
|
||||
# An encrypted connection with HTTPS is highly recommended
|
||||
# because tokens may be transferred in GET requests.
|
||||
|
||||
# The port that your Misskey server should listen on.
|
||||
url: http://localhost:3000
|
||||
port: 3000
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
db:
|
||||
host: postgres
|
||||
port: 5432
|
||||
|
||||
# Database name
|
||||
db: postgres
|
||||
|
||||
# Auth
|
||||
user: postgres
|
||||
pass: test
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl: true
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
db: firefish_db
|
||||
user: firefish
|
||||
pass: password
|
||||
redis:
|
||||
host: redis
|
||||
port: 6379
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
||||
# You can select the ID generation method.
|
||||
# You don't usually need to change this setting, but you can
|
||||
# change it according to your preferences.
|
||||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Max note length, should be < 8000.
|
||||
#maxNoteLength: 3000
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
# Syslog option
|
||||
#syslog:
|
||||
# host: localhost
|
||||
# port: 514
|
||||
|
||||
# Proxy for HTTP/HTTPS
|
||||
#proxy: http://127.0.0.1:3128
|
||||
|
||||
#proxyBypassHosts: [
|
||||
# 'example.com',
|
||||
# '192.0.2.8'
|
||||
#]
|
||||
|
||||
# Proxy for SMTP/SMTPS
|
||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
#proxyRemoteFiles: true
|
||||
|
||||
#allowedPrivateNetworks: [
|
||||
# '127.0.0.1/32'
|
||||
#]
|
||||
|
||||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
||||
|
||||
# Managed hosting settings
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
# Each category is optional, but if each item in each category is mandatory!
|
||||
# If you mess this up, that's on you, you've been warned...
|
||||
|
||||
#maxUserSignups: 100
|
||||
#isManagedHosting: true
|
||||
#deepl:
|
||||
# managed: true
|
||||
# authKey: ''
|
||||
# isPro: false
|
||||
#
|
||||
#email:
|
||||
# managed: true
|
||||
# address: 'example@email.com'
|
||||
# host: 'email.com'
|
||||
# port: 587
|
||||
# user: 'example@email.com'
|
||||
# pass: ''
|
||||
# useImplicitSslTls: false
|
||||
#
|
||||
#objectStorage:
|
||||
# managed: true
|
||||
# baseUrl: ''
|
||||
# bucket: ''
|
||||
# prefix: ''
|
||||
# endpoint: ''
|
||||
# region: ''
|
||||
# accessKey: ''
|
||||
# secretKey: ''
|
||||
# useSsl: true
|
||||
# connnectOverProxy: false
|
||||
# setPublicReadOnUpload: true
|
||||
# s3ForcePathStyle: true
|
||||
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
|
||||
# Seriously. Do NOT fill out the above settings if you're self-hosting.
|
||||
# They're much better off being set from the control panel.
|
||||
|
|
|
@ -13,16 +13,8 @@ redis:
|
|||
host: firefish_redis
|
||||
port: 6379
|
||||
|
||||
id: 'aid'
|
||||
|
||||
#allowedPrivateNetworks: [
|
||||
# '10.69.1.0/24'
|
||||
#]
|
||||
|
||||
logLevel: [
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
'debug',
|
||||
'info'
|
||||
]
|
||||
maxLogLevel: 'debug'
|
||||
|
|
|
@ -145,16 +145,11 @@ reservedUsernames: [
|
|||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
# Log Option
|
||||
# Production env: ['error', 'success', 'warning', 'info']
|
||||
# Debug/Test env or Troubleshooting: ['error', 'success', 'warning', 'debug' ,'info']
|
||||
# Production env which storage space or IO is tight: ['error', 'warning']
|
||||
logLevel: [
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
'info'
|
||||
]
|
||||
# Log level (error, warning, info, debug, trace)
|
||||
# Production env: info
|
||||
# Production env whose storage space or IO is tight: warning
|
||||
# Debug/Test env or Troubleshooting: debug (or trace)
|
||||
maxLogLevel: info
|
||||
|
||||
# Syslog option
|
||||
#syslog:
|
||||
|
@ -178,19 +173,13 @@ logLevel: [
|
|||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
# Proxy remote files (default: true)
|
||||
#proxyRemoteFiles: true
|
||||
|
||||
#allowedPrivateNetworks: [
|
||||
# '127.0.0.1/32'
|
||||
#]
|
||||
|
||||
# TWA
|
||||
#twa:
|
||||
# nameSpace: android_app
|
||||
# packageName: tld.domain.twa
|
||||
# sha256CertFingerprints: ['AB:CD:EF']
|
||||
|
||||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ node_modules
|
|||
report.*.json
|
||||
|
||||
# Rust
|
||||
packages/backend-rs/target
|
||||
/target
|
||||
|
||||
# Coverage
|
||||
coverage
|
||||
|
@ -51,12 +51,11 @@ title.svg
|
|||
/dev
|
||||
/docs
|
||||
/scripts
|
||||
!/scripts/copy-assets.mjs
|
||||
biome.json
|
||||
COPYING
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
Dockerfile
|
||||
LICENSE
|
||||
Procfile
|
||||
README.md
|
||||
SECURITY.md
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,6 +14,9 @@ packages/backend/.idea/vcs.xml
|
|||
node_modules
|
||||
report.*.json
|
||||
|
||||
# Cargo
|
||||
/target
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
|
255
.gitlab-ci.yml
Normal file
255
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,255 @@
|
|||
image: docker.io/rust:slim-bookworm
|
||||
|
||||
services:
|
||||
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
|
||||
alias: postgres
|
||||
pull_policy: if-not-present
|
||||
- name: docker.io/redis:7-alpine
|
||||
alias: redis
|
||||
pull_policy: if-not-present
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == 'firefish/firefish' || $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- locales/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
stages:
|
||||
- dependency
|
||||
- test
|
||||
- build
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: 'firefish_db'
|
||||
POSTGRES_USER: 'firefish'
|
||||
POSTGRES_PASSWORD: 'password'
|
||||
POSTGRES_HOST_AUTH_METHOD: 'trust'
|
||||
DEBIAN_FRONTEND: 'noninteractive'
|
||||
CARGO_PROFILE_DEV_OPT_LEVEL: '0'
|
||||
CARGO_PROFILE_DEV_LTO: 'off'
|
||||
CARGO_PROFILE_DEV_DEBUG: 'none'
|
||||
CARGO_TERM_COLOR: 'always'
|
||||
GIT_CLEAN_FLAGS: -ffdx -e node_modules/ -e built/ -e target/ -e packages/backend-rs/built/
|
||||
|
||||
default:
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential clang mold python3 perl nodejs postgresql-client
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
|
||||
- export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
|
||||
test:build:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: always
|
||||
needs:
|
||||
- job: cargo:clippy
|
||||
optional: true
|
||||
- job: cargo:test
|
||||
optional: true
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
test:build:backend_ts_only:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend/**/*
|
||||
- packages/firefish-js/**/*
|
||||
- packages/megalodon/**/*
|
||||
when: always
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential python3 nodejs postgresql-client
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- mkdir -p packages/backend-rs/built
|
||||
- cp packages/backend-rs/index.js packages/backend-rs/built/index.js
|
||||
- cp packages/backend-rs/index.d.ts packages/backend-rs/built/index.d.ts
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
- export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'backend' --filter 'firefish-js' --filter 'megalodon' run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
test:build:client_only:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/client/**/*
|
||||
- packages/firefish-js/**/*
|
||||
- packages/sw/**/*
|
||||
- locales/**/*
|
||||
when: always
|
||||
services: []
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential python3 perl nodejs
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'firefish-js' --filter 'client' --filter 'sw' run build:debug
|
||||
|
||||
build:container:
|
||||
stage: build
|
||||
image: quay.io/buildah/stable:latest
|
||||
services: []
|
||||
rules:
|
||||
- if: $BUILD == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- locales/**/*
|
||||
- scripts/copy-assets.mjs
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
when: always
|
||||
needs:
|
||||
- job: test:build
|
||||
optional: true
|
||||
- job: test:build:backend_ts_only
|
||||
optional: true
|
||||
- job: test:build:client_only
|
||||
optional: true
|
||||
variables:
|
||||
STORAGE_DRIVER: overlay2
|
||||
before_script:
|
||||
- buildah version
|
||||
- buildah prune --all --force || true
|
||||
- echo "${CI_REGISTRY_PASSWORD}" | buildah login --username "${CI_REGISTRY_USER}" --password-stdin "${CI_REGISTRY}"
|
||||
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
|
||||
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
|
||||
script:
|
||||
- |-
|
||||
buildah build \
|
||||
--platform linux/amd64 \
|
||||
--layers \
|
||||
--cache-to "${IMAGE_CACHE}" \
|
||||
--cache-from "${IMAGE_CACHE}" \
|
||||
--tag "${IMAGE_TAG}" \
|
||||
.
|
||||
- buildah inspect "${IMAGE_TAG}"
|
||||
- buildah push "${IMAGE_TAG}"
|
||||
|
||||
cargo:test:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- package.json
|
||||
when: always
|
||||
script:
|
||||
- curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin
|
||||
- cargo test --doc
|
||||
- cargo nextest run
|
||||
|
||||
cargo:clippy:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: always
|
||||
services: []
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get install -y --no-install-recommends build-essential clang mold perl
|
||||
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
|
||||
- rustup component add clippy
|
||||
script:
|
||||
- cargo clippy -- -D warnings
|
||||
|
||||
renovate:
|
||||
stage: dependency
|
||||
image:
|
||||
name: docker.io/renovate/renovate:37-slim
|
||||
entrypoint: [""]
|
||||
rules:
|
||||
- if: $RENOVATE && $CI_PIPELINE_SOURCE == 'schedule'
|
||||
services: []
|
||||
before_script: []
|
||||
script:
|
||||
- renovate --platform gitlab --token "${API_TOKEN}" --endpoint "${CI_SERVER_URL}/api/v4" "${CI_PROJECT_PATH}"
|
|
@ -3,27 +3,47 @@
|
|||
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md)
|
||||
🤝 By submitting this issue, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -->
|
||||
|
||||
**What happened?** _(Please give us a brief description of what happened.)_
|
||||
## What happened? <!-- Please give us a brief description of what happened. -->
|
||||
|
||||
**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_
|
||||
|
||||
**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_
|
||||
## What did you expect to happen? <!-- Please give us a brief description of what you expected to happen. -->
|
||||
|
||||
**Instance** _(What instance of firefish are you using?)_
|
||||
|
||||
**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_
|
||||
## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
|
||||
|
||||
**What browser are you using? (Client-side issues only)**
|
||||
|
||||
**What operating system are you using? (Client-side issues only)**
|
||||
## What type of issue is this? <!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
|
||||
|
||||
**How do you deploy Firefish on your server? (Server-side issues only)**
|
||||
- [ ] server-side
|
||||
- [ ] client-side
|
||||
- [ ] not sure
|
||||
|
||||
**What operating system are you using? (Server-side issues only)**
|
||||
<details>
|
||||
|
||||
**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_
|
||||
### Instance <!-- What instance of firefish are you using? -->
|
||||
|
||||
**Contribution Guidelines**
|
||||
|
||||
### What browser are you using? (client-side issues only)
|
||||
|
||||
|
||||
### What operating system are you using? (client-side issues only)
|
||||
|
||||
|
||||
### How do you deploy Firefish on your server? (server-side issues only)
|
||||
|
||||
|
||||
### What operating system are you using? (Server-side issues only)
|
||||
|
||||
|
||||
### Relevant log output <!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. -->
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
## Contribution Guidelines
|
||||
By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] I agree to follow this project's Contribution Guidelines
|
||||
- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
|
||||
|
||||
## Are you willing to fix this bug? (optional)
|
||||
- [ ] Yes. I will fix this bug and open a merge request if the change is agreed upon.
|
||||
|
|
|
@ -3,15 +3,22 @@
|
|||
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://firefish.dev/firefish/firefish/-/blob/develop/SECURITY.md)
|
||||
🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md) -->
|
||||
|
||||
**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_
|
||||
## What feature would you like implemented? <!-- Please give us a brief description of what you'd like. -->
|
||||
|
||||
**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_
|
||||
|
||||
**Version** _(What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.)_
|
||||
## Why should we add this feature? <!-- Please give us a brief description of why your feature is important. -->
|
||||
|
||||
**Instance** _(What instance of firefish are you using?)_
|
||||
|
||||
**Contribution Guidelines**
|
||||
## Version <!-- What version of firefish is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information. -->
|
||||
|
||||
|
||||
## Instance <!-- What instance of firefish are you using? -->
|
||||
|
||||
|
||||
## Contribution Guidelines
|
||||
By submitting this issue, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] I agree to follow this project's Contribution Guidelines
|
||||
- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
|
||||
|
||||
## Are you willing to implement this feature? (optional)
|
||||
- [ ] Yes. I will implement this feature and open a merge request if the change is agreed upon.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<!-- Thanks for taking the time to make Firefish better! It's not required, but please consider using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) when making your commits. If you use VSCode, please use the [Conventional Commits extension](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits). -->
|
||||
|
||||
**What does this PR do?** _(Please give us a brief description of what this PR does.)_
|
||||
## What does this PR do? <!-- Please give us a brief description of what this PR does. -->
|
||||
|
||||
**Contribution Guidelines**
|
||||
|
||||
## Contribution Guidelines
|
||||
By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] This change is reviewed in an issue / This is a minor bug fix
|
||||
- [ ] I agree to follow this project's Contribution Guidelines
|
||||
- [ ] I have made sure to test this pull request
|
||||
- [ ] I have made sure to run `pnpm run format` before submitting this pull request
|
||||
|
@ -12,4 +14,4 @@ If this merge request makes changes to the Firefish API, please update `docs/api
|
|||
- [ ] I updated the document / This merge request doesn't include API changes
|
||||
|
||||
<!-- Uncomment if your merge request has multiple authors -->
|
||||
<!-- Co-authored-by: Name <email@email.com> -->
|
||||
<!-- Co-authored-by: Name <email@example.com> -->
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
## Checklist
|
||||
|
||||
- [ ] There are no pending changes on Weblate
|
||||
|
||||
I have updated...
|
||||
|
||||
- [ ] `package.json`
|
||||
- [ ] `docs/changelog.md`
|
||||
- [ ] `docs/notice-for-admins.md`
|
||||
- [ ] `docs/api-change.md`
|
||||
- [ ] `packages/backend-rs/index.js`
|
||||
- [ ] OCI container image
|
||||
|
||||
<!-- TODO: Add automated tests (task runners are currently down) -->
|
||||
|
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
@ -12,6 +12,7 @@
|
|||
"esbenp.prettier-vscode",
|
||||
"redhat.vscode-yaml",
|
||||
"yoavbls.pretty-ts-errors",
|
||||
"biomejs.biome"
|
||||
"biomejs.biome",
|
||||
"rust-lang.rust-analyzer"
|
||||
]
|
||||
}
|
||||
|
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"packages/backend-rs/Cargo.toml"
|
||||
]
|
||||
}
|
10
COPYING
10
COPYING
|
@ -1,6 +1,7 @@
|
|||
Unless specified otherwise, the entirety of this repository is subject to the following:
|
||||
Copyright © 2014-2023 syuilo and contributors
|
||||
Copyright © 2022-2023 Kainoa Kanter and contributors
|
||||
Copyright © 2024 Firefish contributors
|
||||
|
||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
|
||||
|
||||
|
@ -13,6 +14,7 @@ These specific configuration directories:
|
|||
|
||||
and their contents are
|
||||
Copyright © 2022-2023 Kainoa Kanter and contributors
|
||||
Copyright © 2024 Firefish contributors
|
||||
|
||||
And are distributed under The Apache License, Version 2.0, you should have received a copy of the license file as LICENSE in each specified directory.
|
||||
|
||||
|
@ -24,14 +26,14 @@ RsaSignature2017 implementation by Transmute Industries Inc
|
|||
License: MIT
|
||||
https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE
|
||||
|
||||
Machine learning model for sensitive images by Infinite Red, Inc.
|
||||
License: MIT
|
||||
https://github.com/infinitered/nsfwjs/blob/master/LICENSE
|
||||
|
||||
Chiptune2.js by Simon Gündling
|
||||
License: MIT
|
||||
https://github.com/deskjet/chiptune2.js#license
|
||||
|
||||
bb8-redis by Kyle Huey
|
||||
License: MIT
|
||||
https://github.com/djc/bb8/blob/62597aa45ac1746780b08cb6a68cf7d65452a23a/LICENSE
|
||||
|
||||
Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository:
|
||||
|
||||
pnpm licenses list
|
||||
|
|
2434
packages/backend-rs/Cargo.lock → Cargo.lock
generated
2434
packages/backend-rs/Cargo.lock → Cargo.lock
generated
File diff suppressed because it is too large
Load diff
51
Cargo.toml
Normal file
51
Cargo.toml
Normal file
|
@ -0,0 +1,51 @@
|
|||
[workspace]
|
||||
members = ["packages/backend-rs", "packages/macro-rs"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
macro-rs = { path = "packages/macro-rs" }
|
||||
|
||||
napi = { version = "2.16.6", default-features = false }
|
||||
napi-derive = "2.16.5"
|
||||
napi-build = "2.1.3"
|
||||
|
||||
argon2 = "0.5.3"
|
||||
async-trait = "0.1.80"
|
||||
basen = "0.1.0"
|
||||
bb8 = "0.8.3"
|
||||
bcrypt = "0.15.1"
|
||||
chrono = "0.4.38"
|
||||
convert_case = "0.6.0"
|
||||
cuid2 = "0.1.2"
|
||||
emojis = "0.6.2"
|
||||
idna = "0.5.0"
|
||||
image = "0.25.1"
|
||||
isahc = "1.7.2"
|
||||
nom-exif = "1.2.0"
|
||||
once_cell = "1.19.0"
|
||||
openssl = "0.10.64"
|
||||
pretty_assertions = "1.4.0"
|
||||
proc-macro2 = "1.0.82"
|
||||
quote = "1.0.36"
|
||||
rand = "0.8.5"
|
||||
redis = { version = "0.25.3", default-features = false }
|
||||
regex = "1.10.4"
|
||||
rmp-serde = "1.3.0"
|
||||
sea-orm = "0.12.15"
|
||||
serde = "1.0.202"
|
||||
serde_json = "1.0.117"
|
||||
serde_yaml = "0.9.34"
|
||||
strum = "0.26.2"
|
||||
syn = "2.0.64"
|
||||
sysinfo = "0.30.12"
|
||||
thiserror = "1.0.61"
|
||||
tokio = "1.37.0"
|
||||
tokio-test = "0.4.4"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
url = "2.5.0"
|
||||
urlencoding = "2.1.3"
|
||||
web-push = { git = "https://github.com/pimeys/rust-web-push", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
55
Dockerfile
55
Dockerfile
|
@ -3,42 +3,53 @@ FROM docker.io/node:20-alpine as build
|
|||
WORKDIR /firefish
|
||||
|
||||
# Install compilation dependencies
|
||||
RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3
|
||||
RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3 perl
|
||||
RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# Copy only the cargo dependency-related files first, to cache efficiently
|
||||
COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml
|
||||
COPY packages/backend-rs/Cargo.lock packages/backend-rs/Cargo.lock
|
||||
COPY packages/backend-rs/src/lib.rs packages/backend-rs/src/
|
||||
# Copy only backend-rs dependency-related files first, to cache efficiently
|
||||
COPY package.json pnpm-workspace.yaml ./
|
||||
COPY packages/backend-rs/package.json packages/backend-rs/package.json
|
||||
COPY packages/backend-rs/npm/linux-x64-musl/package.json packages/backend-rs/npm/linux-x64-musl/package.json
|
||||
COPY packages/backend-rs/npm/linux-arm64-musl/package.json packages/backend-rs/npm/linux-arm64-musl/package.json
|
||||
|
||||
# Install cargo dependencies
|
||||
RUN cargo fetch --locked --manifest-path /firefish/packages/backend-rs/Cargo.toml
|
||||
COPY Cargo.toml Cargo.toml
|
||||
COPY Cargo.lock Cargo.lock
|
||||
COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml
|
||||
COPY packages/backend-rs/src/lib.rs packages/backend-rs/src/
|
||||
COPY packages/macro-rs/Cargo.toml packages/macro-rs/Cargo.toml
|
||||
COPY packages/macro-rs/src/lib.rs packages/macro-rs/src/
|
||||
|
||||
# Configure pnpm, and install backend-rs dependencies
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm --filter backend-rs install
|
||||
RUN cargo fetch --locked --manifest-path Cargo.toml
|
||||
|
||||
# Copy in the rest of the rust files
|
||||
COPY packages/backend-rs packages/backend-rs/
|
||||
# COPY packages/macro-rs packages/macro-rs/
|
||||
|
||||
# Compile backend-rs
|
||||
RUN NODE_ENV='production' pnpm run --filter backend-rs build
|
||||
|
||||
# Copy/Overwrite index.js to mitigate the bug in napi-rs codegen
|
||||
COPY packages/backend-rs/index.js packages/backend-rs/built/index.js
|
||||
|
||||
# Copy only the dependency-related files first, to cache efficiently
|
||||
COPY package.json pnpm*.yaml ./
|
||||
COPY packages/backend/package.json packages/backend/package.json
|
||||
COPY packages/client/package.json packages/client/package.json
|
||||
COPY packages/sw/package.json packages/sw/package.json
|
||||
COPY packages/firefish-js/package.json packages/firefish-js/package.json
|
||||
COPY packages/megalodon/package.json packages/megalodon/package.json
|
||||
COPY packages/backend-rs/package.json packages/backend-rs/package.json
|
||||
COPY packages/backend-rs/npm/linux-x64-musl/package.json packages/backend-rs/npm/linux-x64-musl/package.json
|
||||
COPY packages/backend-rs/npm/linux-arm64-musl/package.json packages/backend-rs/npm/linux-arm64-musl/package.json
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Configure pnpm, and install dev mode dependencies for compilation
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm install --frozen-lockfile
|
||||
# Install dev mode dependencies for compilation
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy in the rest of the rust files
|
||||
COPY packages/backend-rs packages/backend-rs/
|
||||
|
||||
# Compile backend-rs
|
||||
RUN NODE_ENV='production' pnpm run --filter backend-rs build
|
||||
|
||||
# Copy in the rest of the files to compile
|
||||
# Copy in the rest of the files to build
|
||||
COPY . ./
|
||||
RUN NODE_ENV='production' pnpm run --filter firefish-js build
|
||||
RUN NODE_ENV='production' pnpm run --recursive --parallel --filter '!backend-rs' --filter '!firefish-js' build && pnpm run gulp
|
||||
|
||||
# Build other workspaces
|
||||
RUN NODE_ENV='production' pnpm run --recursive --filter '!backend-rs' build && pnpm run build:assets
|
||||
|
||||
# Trim down the dependencies to only those for production
|
||||
RUN find . -path '*/node_modules/*' -delete && pnpm install --prod --frozen-lockfile
|
||||
|
|
21
Makefile
21
Makefile
|
@ -1,11 +1,9 @@
|
|||
ifneq (dev,$(wildcard config.env))
|
||||
include ./dev/config.env
|
||||
export
|
||||
endif
|
||||
include ./dev/config.env
|
||||
export
|
||||
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: format entities update-index-js
|
||||
pre-commit: format entities napi
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
|
@ -13,12 +11,13 @@ format:
|
|||
|
||||
.PHONY: entities
|
||||
entities:
|
||||
pnpm --filter=backend run build:debug
|
||||
pnpm run migrate
|
||||
$(MAKE) -C ./packages/backend-rs regenerate-entities
|
||||
|
||||
.PHONY: update-index-js
|
||||
update-index-js:
|
||||
$(MAKE) -C ./packages/backend-rs index.js
|
||||
.PHONY: napi
|
||||
napi:
|
||||
$(MAKE) -C ./packages/backend-rs update-index
|
||||
|
||||
|
||||
.PHONY: build
|
||||
|
@ -29,13 +28,13 @@ build:
|
|||
pnpm run migrate
|
||||
|
||||
|
||||
.PHONY: db.init db.up db.down
|
||||
db.init:
|
||||
$(MAKE) -C ./dev/db-container init
|
||||
.PHONY: db.up db.down db.init
|
||||
db.up:
|
||||
$(MAKE) -C ./dev/db-container up
|
||||
db.down:
|
||||
$(MAKE) -C ./dev/db-container down
|
||||
db.init:
|
||||
$(MAKE) -C ./dev/db-container init
|
||||
|
||||
.PHONY: psql redis-cli
|
||||
psql:
|
||||
|
|
27
biome.json
27
biome.json
|
@ -1,12 +1,31 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.0.0/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noUselessElse": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.vue", "packages/client/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useImportType": "warn",
|
||||
"useShorthandFunctionType": "warn",
|
||||
"useTemplate": "warn",
|
||||
"noNonNullAssertion": "off",
|
||||
"useNodejsImportProtocol": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
3
ci/cargo/config.toml
Normal file
3
ci/cargo/config.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "/usr/bin/clang"
|
||||
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]
|
|
@ -1,4 +1,4 @@
|
|||
COMPOSE='docker compose'
|
||||
COMPOSE='podman-compose'
|
||||
POSTGRES_PASSWORD=password
|
||||
POSTGRES_USER=firefish
|
||||
POSTGRES_DB=firefish_db
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
.PHONY: init up down
|
||||
init: down up
|
||||
.PHONY: up down init
|
||||
up:
|
||||
$(COMPOSE) up --detach
|
||||
down:
|
||||
$(COMPOSE) down
|
||||
init:
|
||||
$(COMPOSE) down --volumes
|
||||
$(COMPOSE) up --detach
|
||||
|
||||
.PHONY: psql redis-cli
|
||||
psql:
|
||||
|
|
|
@ -5,6 +5,8 @@ services:
|
|||
image: docker.io/redis:7-alpine
|
||||
ports:
|
||||
- "26379:6379"
|
||||
volumes:
|
||||
- "redis-data:/data"
|
||||
db:
|
||||
image: docker.io/groonga/pgroonga:3.1.8-alpine-12
|
||||
env_file:
|
||||
|
@ -13,3 +15,10 @@ services:
|
|||
- "25432:5432"
|
||||
volumes:
|
||||
- "./install.sql:/docker-entrypoint-initdb.d/install.sql:ro"
|
||||
- "postgres-data:/var/lib/postgresql/data"
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
name: redis-data
|
||||
postgres-data:
|
||||
name: postgres-data
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
- Node.js
|
||||
- pnpm
|
||||
- Rust toolchain
|
||||
- Python 3
|
||||
- Perl
|
||||
- FFmpeg
|
||||
- Container runtime
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
|
@ -31,7 +33,7 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
1. Copy example config file
|
||||
```sh
|
||||
cp dev/config.example.env dev/config.env
|
||||
# If you use container runtime other than Docker, you need to modify the "COMPOSE" variable
|
||||
# If you use container runtime other than Podman, you need to modify the "COMPOSE" variable
|
||||
# vim dev/config.env
|
||||
```
|
||||
1. Create `.config/default.yml` with the following content
|
||||
|
@ -51,12 +53,7 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
host: localhost
|
||||
port: 26379
|
||||
|
||||
logLevel: [
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
'info'
|
||||
]
|
||||
maxlogLevel: 'debug' # or 'trace'
|
||||
```
|
||||
1. Start database containers
|
||||
```sh
|
||||
|
@ -84,6 +81,19 @@ You can refer to [local-installation.md](./local-installation.md) to install the
|
|||
DONE * [core boot] Now listening on port 3000 on http://localhost:3000
|
||||
```
|
||||
|
||||
## Update auto-generated files in `package/backend-rs`
|
||||
|
||||
You need to install `sea-orm-cli` to regenerate database entities.
|
||||
|
||||
```sh
|
||||
cargo install sea-orm-cli
|
||||
```
|
||||
|
||||
```sh
|
||||
make entities
|
||||
make napi
|
||||
```
|
||||
|
||||
## Reset the environment
|
||||
|
||||
You can recreate a fresh local Firefish environment by recreating the database containers:
|
||||
|
|
|
@ -42,6 +42,8 @@ cargo --version
|
|||
|
||||
### PostgreSQL and PGroonga
|
||||
|
||||
Firefish requires PostgreSQL v12 or later. We recommend that you install v12.x for the same reason as Node.js.
|
||||
|
||||
PostgreSQL install instructions can be found at [this page](https://www.postgresql.org/download/).
|
||||
|
||||
```sh
|
||||
|
@ -139,12 +141,7 @@ sudo apt install ffmpeg
|
|||
host: localhost
|
||||
port: 6379
|
||||
|
||||
logLevel: [
|
||||
'error',
|
||||
'success',
|
||||
'warning',
|
||||
'info'
|
||||
]
|
||||
maxLogLevel: 'debug' # or 'trace'
|
||||
```
|
||||
|
||||
## 4. Build and start Firefish
|
||||
|
|
|
@ -17,6 +17,7 @@ services:
|
|||
# - web
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NODE_OPTIONS: --max-old-space-size=3072
|
||||
volumes:
|
||||
- ./custom:/firefish/custom:ro
|
||||
- ./files:/firefish/files
|
||||
|
|
|
@ -2,6 +2,35 @@
|
|||
|
||||
Breaking changes are indicated by the :warning: icon.
|
||||
|
||||
## v20240516
|
||||
|
||||
- :warning: `server-info` (an endpoint to get server hardware information) now requires credentials.
|
||||
- :warning: `net` (server's default network interface) has been removed from `admin/server-info`.
|
||||
- Adding `lang` to the response of `i` and the request parameter of `i/update`.
|
||||
|
||||
## v20240504
|
||||
|
||||
- :warning: Removed `release` endpoint.
|
||||
|
||||
## v20240424
|
||||
|
||||
- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional).
|
||||
- Added `filter` optional parameter to `notes/renotes` endpoint to filter the types of renotes. It can take the following values:
|
||||
- `all` (default)
|
||||
- `renote`
|
||||
- `quote`
|
||||
- :warning: Removed the following optional parameters in `notes/reactions`, as they were never taken into account due to a bug:
|
||||
- `sinceId`
|
||||
- `untilId`
|
||||
|
||||
## v20240413
|
||||
|
||||
- :warning: Removed `patrons` endpoint.
|
||||
|
||||
## v20240405
|
||||
|
||||
- Added `notes/history` endpoint.
|
||||
|
||||
## v20240319
|
||||
|
||||
- :warning: `followingCount` and `followersCount` in `users/show` will be `null` (instead of 0) if these values are unavailable.
|
||||
|
|
|
@ -5,6 +5,58 @@ Critical security updates are indicated by the :warning: icon.
|
|||
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
|
||||
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
|
||||
|
||||
## [v20240516](https://firefish.dev/firefish/firefish/-/merge_requests/10854/commits)
|
||||
|
||||
- Improve timeline UX (you can restore the original appearance by settings)
|
||||
- Remove `$[center]` MFM function
|
||||
- This function was suddenly added last year (https://firefish.dev/firefish/firefish/-/commit/1a971efa689323d54eebb4d3646e102fb4d1d95a), but according to the [MFM spec](https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md#fn), `$[something]` must be an inline element (while `center` is a block element), so such a syntax is not expected by MFM renderers. Please use `<center></center>` instead.
|
||||
- Fix bugs
|
||||
|
||||
## [v20240504](https://firefish.dev/firefish/firefish/-/merge_requests/10790/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
||||
## :warning: [v20240430](https://firefish.dev/firefish/firefish/-/merge_requests/10781/commits)
|
||||
|
||||
- Add ability to group similar notifications
|
||||
- Add features to share links to an account in the three dots menu on the profile page
|
||||
- Improve server logs
|
||||
- Fix bugs (including a critical security issue)
|
||||
- We are very thankful to @tesaguri and Laura Hausmann for helping to fix the security issue.
|
||||
|
||||
## [v20240424](https://firefish.dev/firefish/firefish/-/merge_requests/10765/commits)
|
||||
|
||||
- Improve the usability of the feature to prevent forgetting to write alt texts
|
||||
- Add a server-wide setting for the maximum number of antennas each user can create
|
||||
- Fix bugs (including a medium severity security issue)
|
||||
- We are very thankful to @mei23 for kindly sharing the information about the security issue.
|
||||
|
||||
## [v20240421](https://firefish.dev/firefish/firefish/-/merge_requests/10756/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
||||
## [v20240413](https://firefish.dev/firefish/firefish/-/merge_requests/10741/commits)
|
||||
|
||||
- Add "Media" tab to user page
|
||||
- Improve federation and rendering of mathematical expressions
|
||||
- Remove donor information from the web client
|
||||
- See also: https://info.firefish.dev/notes/9s1n283sb10rh869
|
||||
- Fix bugs
|
||||
|
||||
## [v20240405](https://firefish.dev/firefish/firefish/-/merge_requests/10733/commits)
|
||||
|
||||
- Add ability to view the history of post edits (!10714)
|
||||
- Fix bugs
|
||||
|
||||
## [v20240401](https://firefish.dev/firefish/firefish/-/merge_requests/10724/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
||||
## :warning: [v20240330](https://firefish.dev/firefish/firefish/-/merge_requests/10719/commits)
|
||||
|
||||
- Fix bugs (including a critical security issue)
|
||||
- We are very thankful to Oneric (the reporter of the security issue) and Laura Hausmann (Iceshrimp maintainer) for kindly and securely sharing the information to fix the issue.
|
||||
|
||||
## [v20240326](https://firefish.dev/firefish/firefish/-/merge_requests/10713/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'UserprofileJsonbToArray1714270605574',
|
||||
'DropUnusedUserprofileColumns1714259023878',
|
||||
'AntennaJsonbToArray1714192520471',
|
||||
'AddUserProfileLanguage1714888400293',
|
||||
'DropUnusedIndexes1714643926317',
|
||||
'AlterAkaType1714099399879',
|
||||
'AddDriveFileUsage1713451569342',
|
||||
'ConvertCwVarcharToText1713225866247',
|
||||
'FixChatFileConstraint1712855579316',
|
||||
'DropTimeZone1712425488543',
|
||||
'ExpandNoteEdit1711936358554',
|
||||
'markLocalFilesNsfwByDefault1709305200000',
|
||||
'FixMutingIndices1710690239308',
|
||||
'NoteFile1710304584214',
|
||||
|
@ -19,6 +30,161 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
'RemoveNativeUtilsMigration1705877093218'
|
||||
);
|
||||
|
||||
-- userprofile-jsonb-to-array
|
||||
ALTER TABLE "user_profile" RENAME COLUMN "mutedInstances" TO "mutedInstances_old";
|
||||
ALTER TABLE "user_profile" ADD COLUMN "mutedInstances" jsonb NOT NULL DEFAULT '[]';
|
||||
UPDATE "user_profile" SET "mutedInstances" = to_jsonb("mutedInstances_old");
|
||||
ALTER TABLE "user_profile" DROP COLUMN "mutedInstances_old";
|
||||
ALTER TABLE "user_profile" RENAME COLUMN "mutedWords" TO "mutedWords_old";
|
||||
ALTER TABLE "user_profile" ADD COLUMN "mutedWords" jsonb NOT NULL DEFAULT '[]';
|
||||
CREATE TEMP TABLE "BCrsGgLCUeMMLARy" ("userId" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]');
|
||||
INSERT INTO "BCrsGgLCUeMMLARy" ("userId", "kws") SELECT "userId", jsonb_agg("X"."w") FROM (SELECT "userId", to_jsonb(string_to_array(unnest("mutedWords_old"), ' ')) AS "w" FROM "user_profile") AS "X" GROUP BY "userId";
|
||||
UPDATE "user_profile" SET "mutedWords" = "kws" FROM "BCrsGgLCUeMMLARy" WHERE "user_profile"."userId" = "BCrsGgLCUeMMLARy"."userId";
|
||||
ALTER TABLE "user_profile" DROP COLUMN "mutedWords_old";
|
||||
|
||||
-- drop-unused-userprofile-columns
|
||||
ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}';
|
||||
COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.';
|
||||
ALTER TABLE "user_profile" ADD "clientData" jsonb NOT NULL DEFAULT '{}';
|
||||
COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.';
|
||||
|
||||
-- antenna-jsonb-to-array
|
||||
UPDATE "antenna" SET "instances" = '{""}' WHERE "instances" = '{}';
|
||||
ALTER TABLE "antenna" RENAME COLUMN "instances" TO "instances_old";
|
||||
ALTER TABLE "antenna" ADD COLUMN "instances" jsonb NOT NULL DEFAULT '[]';
|
||||
UPDATE "antenna" SET "instances" = to_jsonb("instances_old");
|
||||
ALTER TABLE "antenna" DROP COLUMN "instances_old";
|
||||
UPDATE "antenna" SET "keywords" = '{""}' WHERE "keywords" = '{}';
|
||||
ALTER TABLE "antenna" RENAME COLUMN "keywords" TO "keywords_old";
|
||||
ALTER TABLE "antenna" ADD COLUMN "keywords" jsonb NOT NULL DEFAULT '[]';
|
||||
CREATE TEMP TABLE "QvPNcMitBFkqqBgm" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]');
|
||||
INSERT INTO "QvPNcMitBFkqqBgm" ("id", "kws") SELECT "id", jsonb_agg("X"."w") FROM (SELECT "id", to_jsonb(string_to_array(unnest("keywords_old"), ' ')) AS "w" FROM "antenna") AS "X" GROUP BY "id";
|
||||
UPDATE "antenna" SET "keywords" = "kws" FROM "QvPNcMitBFkqqBgm" WHERE "antenna"."id" = "QvPNcMitBFkqqBgm"."id";
|
||||
ALTER TABLE "antenna" DROP COLUMN "keywords_old";
|
||||
UPDATE "antenna" SET "excludeKeywords" = '{""}' WHERE "excludeKeywords" = '{}';
|
||||
ALTER TABLE "antenna" RENAME COLUMN "excludeKeywords" TO "excludeKeywords_old";
|
||||
ALTER TABLE "antenna" ADD COLUMN "excludeKeywords" jsonb NOT NULL DEFAULT '[]';
|
||||
CREATE TEMP TABLE "MZvVSjHzYcGXmGmz" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]');
|
||||
INSERT INTO "MZvVSjHzYcGXmGmz" ("id", "kws") SELECT "id", jsonb_agg("X"."w") FROM (SELECT "id", to_jsonb(string_to_array(unnest("excludeKeywords_old"), ' ')) AS "w" FROM "antenna") AS "X" GROUP BY "id";
|
||||
UPDATE "antenna" SET "excludeKeywords" = "kws" FROM "MZvVSjHzYcGXmGmz" WHERE "antenna"."id" = "MZvVSjHzYcGXmGmz"."id";
|
||||
ALTER TABLE "antenna" DROP COLUMN "excludeKeywords_old";
|
||||
|
||||
-- drop-unused-indexes
|
||||
CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt");
|
||||
CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId");
|
||||
CREATE INDEX "IDX_25dfc71b0369b003a4cd434d0b" ON "note" ("attachedFileTypes");
|
||||
CREATE INDEX "IDX_2710a55f826ee236ea1a62698f" ON "hashtag" ("mentionedUsersCount");
|
||||
CREATE INDEX "IDX_4c02d38a976c3ae132228c6fce" ON "hashtag" ("mentionedRemoteUsersCount");
|
||||
CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds");
|
||||
CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions");
|
||||
CREATE INDEX "IDX_7fa20a12319c7f6dc3aed98c0a" ON "poll" ("userHost");
|
||||
CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags");
|
||||
CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON "notification" ("createdAt");
|
||||
CREATE INDEX "IDX_c8dfad3b72196dd1d6b5db168a" ON "drive_file" ("createdAt");
|
||||
CREATE INDEX "IDX_d57f9030cd3af7f63ffb1c267c" ON "hashtag" ("attachedUsersCount");
|
||||
CREATE INDEX "IDX_e5848eac4940934e23dbc17581" ON "drive_file" ("uri");
|
||||
CREATE INDEX "IDX_fa99d777623947a5b05f394cae" ON "user" ("tags");
|
||||
|
||||
-- alter-aka-type
|
||||
ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld";
|
||||
ALTER TABLE "user" ADD COLUMN "alsoKnownAs" text;
|
||||
UPDATE "user" SET "alsoKnownAs" = array_to_string("alsoKnownAsOld", ',');
|
||||
COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too';
|
||||
ALTER TABLE "user" DROP COLUMN "alsoKnownAsOld";
|
||||
|
||||
-- AddDriveFileUsage
|
||||
ALTER TABLE "drive_file" DROP COLUMN "usageHint";
|
||||
DROP TYPE "drive_file_usage_hint_enum";
|
||||
|
||||
-- convert-cw-varchar-to-text
|
||||
DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
|
||||
ALTER TABLE "note" ALTER COLUMN "cw" TYPE character varying(512);
|
||||
CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2);
|
||||
|
||||
-- fix-chat-file-constraint
|
||||
ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b";
|
||||
ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
|
||||
-- drop-time-zone
|
||||
ALTER TABLE "abuse_user_report" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "access_token" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "access_token" ALTER "lastUsedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "ad" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "ad" ALTER "expiresAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "announcement" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "announcement" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "announcement_read" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "antenna" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "app" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "attestation_challenge" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "auth_session" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "blocking" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "channel" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "channel" ALTER "lastNotedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "channel_following" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "channel_note_pining" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "clip" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "drive_file" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "drive_folder" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "emoji" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "following" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "follow_request" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "gallery_like" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "gallery_post" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "gallery_post" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "instance" ALTER "caughtAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "instance" ALTER "infoUpdatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "instance" ALTER "lastCommunicatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "instance" ALTER "latestRequestReceivedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "instance" ALTER "latestRequestSentAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "messaging_message" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "moderation_log" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "muting" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "muting" ALTER "expiresAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note_edit" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note_favorite" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note_reaction" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note_thread_muting" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "note_watching" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "notification" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "page" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "page" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "page_like" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "password_reset_request" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "poll" ALTER "expiresAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "poll_vote" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "promo_note" ALTER "expiresAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "promo_read" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "registration_ticket" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "registry_item" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "registry_item" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "renote_muting" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "reply_muting" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "signin" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "sw_subscription" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "used_username" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user" ALTER "lastActiveDate" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user" ALTER "lastFetchedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user" ALTER "updatedAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_group" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_group_invitation" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_group_invite" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_group_joining" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_ip" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_list" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_list_joining" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_note_pining" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_pending" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "user_security_key" ALTER "lastUsed" TYPE timestamp with time zone;
|
||||
ALTER TABLE "webhook" ALTER "createdAt" TYPE timestamp with time zone;
|
||||
ALTER TABLE "webhook" ALTER "latestSentAt" TYPE timestamp with time zone;
|
||||
|
||||
-- expand-note-edit
|
||||
ALTER TABLE "note_edit" DROP COLUMN "emojis";
|
||||
|
||||
-- markLocalFilesNsfwByDefault
|
||||
ALTER TABLE "meta" DROP COLUMN "markLocalFilesNsfwByDefault";
|
||||
|
||||
|
@ -641,9 +807,6 @@ CREATE SEQUENCE public.__chart_day__users_id_seq
|
|||
CACHE 1;
|
||||
ALTER SEQUENCE public.__chart_day__users_id_seq OWNED BY public.__chart_day__users.id;
|
||||
|
||||
-- drop-user-profile-language
|
||||
ALTER TABLE "user_profile" ADD COLUMN "lang" character varying(32);
|
||||
|
||||
-- emoji-moderator
|
||||
ALTER TABLE "user" DROP COLUMN "emojiModPerm";
|
||||
DROP TYPE "public"."user_emojimodperm_enum";
|
||||
|
|
|
@ -33,6 +33,13 @@ docker pull registry.firefish.dev/firefish/firefish:latest
|
|||
# or podman pull registry.firefish.dev/firefish/firefish:latest
|
||||
```
|
||||
|
||||
## Enable database extension
|
||||
|
||||
```sh
|
||||
docker-compose up db --detach && sleep 5 && docker-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
# or podman-compose up db --detach && sleep 5 && podman-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
|
|
|
@ -1,9 +1,37 @@
|
|||
# Install Firefish
|
||||
|
||||
This document shows an example procedure for installing Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
|
||||
Firefish depends on the following software.
|
||||
|
||||
## Runtime dependencies
|
||||
|
||||
- At least [NodeJS](https://nodejs.org/en/) v18.17.0 (v20/v21 recommended)
|
||||
- At least [PostgreSQL](https://www.postgresql.org/) v12 (v16 recommended) with [PGroonga](https://pgroonga.github.io/) extension
|
||||
- At least [Redis](https://redis.io/) v7
|
||||
- Web Proxy (one of the following)
|
||||
- Caddy (recommended)
|
||||
- Nginx (recommended)
|
||||
- Apache
|
||||
- [FFmpeg](https://ffmpeg.org/) for video transcoding (**optional**)
|
||||
- Caching server (**optional**, one of the following)
|
||||
- [DragonflyDB](https://www.dragonflydb.io/)
|
||||
- [KeyDB](https://keydb.dev/)
|
||||
- Another [Redis](https://redis.io/) server
|
||||
|
||||
## Build dependencies
|
||||
|
||||
- At least [Rust](https://www.rust-lang.org/) v1.74
|
||||
- C/C++ compiler & build tools
|
||||
- `build-essential` on Debian/Ubuntu Linux
|
||||
- `base-devel` on Arch Linux
|
||||
- [Python 3](https://www.python.org/)
|
||||
- [Perl](https://www.perl.org/)
|
||||
|
||||
This document shows an example procedure for installing these dependencies and Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
|
||||
|
||||
If you want to use the pre-built container image, please refer to [`install-container.md`](./install-container.md).
|
||||
|
||||
If you do not prepare your environment as document, be sure to meet the minimum dependencies given at the bottom of the page.
|
||||
|
||||
Make sure that you can use the `sudo` command before proceeding.
|
||||
|
||||
## 1. Install dependencies
|
||||
|
@ -154,7 +182,7 @@ sudo apt install ffmpeg
|
|||
1. Build
|
||||
```sh
|
||||
pnpm install --frozen-lockfile
|
||||
NODE_ENV=production pnpm run build
|
||||
NODE_ENV=production NODE_OPTIONS='--max-old-space-size=3072' pnpm run build
|
||||
```
|
||||
1. Execute database migrations
|
||||
```sh
|
||||
|
@ -242,6 +270,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi
|
|||
WorkingDirectory=/home/firefish/firefish
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="npm_config_cache=/tmp"
|
||||
Environment="NODE_OPTIONS=--max-old-space-size=3072"
|
||||
# uncomment the following line if you use jemalloc (note that the path varies on different environments)
|
||||
# Environment="LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2"
|
||||
StandardOutput=journal
|
||||
|
@ -297,7 +326,7 @@ cd ~/firefish
|
|||
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
|
||||
- To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there.
|
||||
- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory.
|
||||
- To update custom assets without rebuilding, just run `pnpm run gulp`.
|
||||
- To update custom assets without rebuilding, just run `pnpm run build:assets`.
|
||||
- To block ChatGPT, CommonCrawl, or other crawlers from indexing your instance, uncomment the respective rules in `./custom/robots.txt`.
|
||||
|
||||
## Tips & Tricks
|
||||
|
|
|
@ -2,6 +2,66 @@
|
|||
|
||||
You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
|
||||
|
||||
## v20240516
|
||||
|
||||
### For all users
|
||||
|
||||
Firefish is now compatible with [Node v22](https://nodejs.org/en/blog/announcements/v22-release-announce). The pre-built OCI container image will still be using the latest LTS version (v20.13.1 as of now).
|
||||
|
||||
## v20240430
|
||||
|
||||
### For all users
|
||||
|
||||
You can control the verbosity of the server log by adding `maxLogLevel` in `.config/default.yml`. `logLevels` has been deprecated in favor of this setting. (see also: <https://firefish.dev/firefish/firefish/-/blob/eac0c1c47cd23789dcc395ab08b074934409fd96/.config/example.yml#L152>)
|
||||
|
||||
### For systemd/pm2 users
|
||||
|
||||
- You need to install Perl to build Firefish. Since Git depends on Perl in many packaging systems, you probably already have Perl installed on your system. You can check the Perl version by this command:
|
||||
```sh
|
||||
perl --version
|
||||
```
|
||||
- Not only Firefish but also Node.js has recently fixed a few security issues:
|
||||
- https://nodejs.org/en/blog/vulnerability/april-2024-security-releases
|
||||
- https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
|
||||
So, it is highly recommended that you upgrade your Node.js version as well. The new versions are
|
||||
- Node v18.20.2 (v18.x LTS)
|
||||
- Node v20.12.2 (v20.x LTS)
|
||||
- Node v21.7.3 (v21.x)
|
||||
|
||||
You can check your Node.js version by this command:
|
||||
```sh
|
||||
node --version
|
||||
```
|
||||
[Node v22](https://nodejs.org/en/blog/announcements/v22-release-announce) was also released several days ago, but we have not yet tested Firefish with this version.
|
||||
|
||||
## v20240413
|
||||
|
||||
### For all users
|
||||
|
||||
Upgrading may take a long time due to the large changes in the database. Please make sure to perform the operations when you have time.
|
||||
|
||||
The time required to upgrade varies greatly depending on the database size and the environment. For reference, we have checked that the database migration takes
|
||||
|
||||
- 70 seconds if the database stores 600,000 posts
|
||||
- 28 minutes if the database stores 12,000,000 posts
|
||||
|
||||
(i.e., it takes roughly (𝑛 / 470,000) minutes where 𝑛 is the number of posts) on a server with 2 GB of RAM. You may want to tweak your database configuration (`postgres.conf`) if the process is significantly slower than our experimental result.
|
||||
|
||||
The number of posts stored on your database can be found at `https://yourserver.example.com/admin/database` (or `notesCount` of `stats` API response).
|
||||
|
||||
### For systemd/pm2 users
|
||||
|
||||
- Please remove `packages/backend-rs/target` before building Firefish.
|
||||
```sh
|
||||
rm --recursive --force packages/backend-rs/target
|
||||
```
|
||||
- Please do not terminate `pnpm run migrate` even if it appears to be frozen.
|
||||
|
||||
### For Docker/Podman users
|
||||
|
||||
You may not be able to access your server for a while after starting the container.
|
||||
|
||||
## v20240326
|
||||
|
||||
### For Docker/Podman users
|
||||
|
@ -19,7 +79,7 @@ The full-text search engine used in Firefish has been changed to [PGroonga](http
|
|||
|
||||
[Edit (2024/03/23 23:55 UTC+9)] ~~**Warning**: You may fail to install PGroonga, since the package registry of Apache Arrow (one of the subdependencies of PGroonga) is currently down ([GitHub issue](https://github.com/apache/arrow/issues/40759)). We recommend that you hold off on upgrading until this problem is resolved.~~
|
||||
|
||||
[Edit (2025/03/25 22:31 UTC+9)] The Apache Arrow repository is now back up and running again.
|
||||
[Edit (2024/03/25 22:31 UTC+9)] The Apache Arrow repository is now back up and running again.
|
||||
|
||||
#### 1. Install PGroonga
|
||||
|
||||
|
@ -31,11 +91,11 @@ psql (PostgreSQL) 16.1
|
|||
|
||||
In this case, your PostgreSQL major version is `16`.
|
||||
|
||||
There are official installation instructions for many operating systems on <https://pgroonga.github.io/install>, so please follow the instructions on this page. However, since many users are using Ubuntu, and there are no instructions for Arch Linux and Fedora, we explicitly list the instructions for Ubuntu, Arch Linux and Fedora here. Please keep in mind that this is not official information and the procedures may change.
|
||||
There are official installation instructions for many operating systems on <https://pgroonga.github.io/install>, so please follow the instructions on this page. However, since many users are using Ubuntu LTS or Debian, and there are no instructions for Arch Linux and Fedora, we explicitly list the instructions for Ubuntu LTS, Debian, Arch Linux and Fedora here. Please keep in mind that this is not official information and the procedures may change.
|
||||
|
||||
##### Ubuntu
|
||||
##### Ubuntu LTS
|
||||
|
||||
1. Add apt repository
|
||||
1. Install subdependencies
|
||||
```sh
|
||||
sudo apt install -y software-properties-common
|
||||
sudo add-apt-repository -y universe
|
||||
|
@ -43,14 +103,35 @@ There are official installation instructions for many operating systems on <http
|
|||
sudo apt install -y wget lsb-release
|
||||
wget https://packages.groonga.org/ubuntu/groonga-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
sudo apt install -y -V ./groonga-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release --codename --short)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
```
|
||||
2. Install PGroonga
|
||||
2. Install PGroonga (replace `16` with your PostgreSQL version)
|
||||
```sh
|
||||
# Please replace "16" with your PostgreSQL major version
|
||||
sudo apt install postgresql-16-pgdg-pgroonga
|
||||
|
||||
# Depending on your PostgreSQL installation method,
|
||||
# the above command may fail and you need to run
|
||||
# the following instead:
|
||||
# sudo apt install postgresql-16-pgroonga
|
||||
```
|
||||
|
||||
##### Debian
|
||||
|
||||
1. Install subdependencies
|
||||
```sh
|
||||
sudo apt install -y -V ca-certificates lsb-release wget
|
||||
wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
sudo apt install -y -V ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
wget https://packages.groonga.org/debian/groonga-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
sudo apt install -y -V ./groonga-apt-source-latest-$(lsb_release --codename --short).deb
|
||||
```
|
||||
2. Install PGroonga (replace `16` with your PostgreSQL version)
|
||||
```sh
|
||||
sudo apt install postgresql-16-pgdg-pgroonga
|
||||
|
||||
# Depending on your PostgreSQL installation method,
|
||||
# the above command may fail and you need to run
|
||||
# the following instead:
|
||||
# sudo apt install postgresql-16-pgroonga
|
||||
```
|
||||
|
||||
##### Arch Linux
|
||||
|
@ -131,8 +212,8 @@ db:
|
|||
After that, execute this command to enable PGroonga:
|
||||
|
||||
```sh
|
||||
docker-compose up db --detach && docker-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
# or podman-compose up db --detach && podman-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
docker-compose up db --detach && sleep 5 && docker-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
# or podman-compose up db --detach && sleep 5 && podman-compose exec db sh -c 'psql --user="${POSTGRES_USER}" --dbname="${POSTGRES_DB}" --command="CREATE EXTENSION pgroonga;"'
|
||||
```
|
||||
|
||||
Once this is done, you can start Firefish as usual.
|
||||
|
@ -226,7 +307,7 @@ A new setting item has been added to control the log levels, so please consider
|
|||
### For systemd/pm2 users
|
||||
|
||||
- Required Rust version has been bumped from v1.68 to v1.70.
|
||||
- `libvips` is no longer required (unless your server os is *BSD), so you may uninstall it from your system. Make sure to execute the following commands after that:
|
||||
- `libvips` is no longer required (unless your server OS is *BSD), so you may uninstall it from your system. Make sure to execute the following commands after that:
|
||||
```sh
|
||||
pnpm clean-npm
|
||||
pnpm install
|
||||
|
|
101
gulpfile.js
101
gulpfile.js
|
@ -1,101 +0,0 @@
|
|||
/**
|
||||
* Gulp tasks
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const gulp = require("gulp");
|
||||
const replace = require("gulp-replace");
|
||||
const terser = require("gulp-terser");
|
||||
const cssnano = require("gulp-cssnano");
|
||||
|
||||
const meta = require("./package.json");
|
||||
|
||||
gulp.task("copy:backend:views", () =>
|
||||
gulp
|
||||
.src("./packages/backend/src/server/web/views/**/*")
|
||||
.pipe(gulp.dest("./packages/backend/built/server/web/views")),
|
||||
);
|
||||
|
||||
gulp.task("copy:backend:custom", () =>
|
||||
gulp
|
||||
.src("./custom/assets/**/*")
|
||||
.pipe(gulp.dest("./packages/backend/assets/")),
|
||||
);
|
||||
|
||||
gulp.task("copy:client:fonts", () =>
|
||||
gulp
|
||||
.src("./packages/client/node_modules/three/examples/fonts/**/*")
|
||||
.pipe(gulp.dest("./built/_client_dist_/fonts/")),
|
||||
);
|
||||
|
||||
gulp.task("copy:client:locales", async (cb) => {
|
||||
fs.mkdirSync("./built/_client_dist_/locales", { recursive: true });
|
||||
const { default: locales } = await import("./locales/index.mjs");
|
||||
|
||||
const v = { _version_: meta.version };
|
||||
|
||||
for (const [lang, locale] of Object.entries(locales)) {
|
||||
fs.writeFileSync(
|
||||
`./built/_client_dist_/locales/${lang}.${meta.version}.json`,
|
||||
JSON.stringify({ ...locale, ...v }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
|
||||
gulp.task("build:backend:script", async () => {
|
||||
const { default: locales } = await import("./locales/index.mjs");
|
||||
|
||||
return gulp
|
||||
.src([
|
||||
"./packages/backend/src/server/web/boot.js",
|
||||
"./packages/backend/src/server/web/bios.js",
|
||||
"./packages/backend/src/server/web/cli.js",
|
||||
])
|
||||
.pipe(replace("SUPPORTED_LANGS", JSON.stringify(Object.keys(locales))))
|
||||
.pipe(
|
||||
terser({
|
||||
toplevel: true,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest("./packages/backend/built/server/web/"));
|
||||
});
|
||||
|
||||
gulp.task("build:backend:style", () => {
|
||||
return gulp
|
||||
.src([
|
||||
"./packages/backend/src/server/web/style.css",
|
||||
"./packages/backend/src/server/web/bios.css",
|
||||
"./packages/backend/src/server/web/cli.css",
|
||||
])
|
||||
.pipe(
|
||||
cssnano({
|
||||
zindex: false,
|
||||
}),
|
||||
)
|
||||
.pipe(gulp.dest("./packages/backend/built/server/web/"));
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
"build",
|
||||
gulp.parallel(
|
||||
"copy:client:locales",
|
||||
"copy:backend:views",
|
||||
"copy:backend:custom",
|
||||
"build:backend:script",
|
||||
"build:backend:style",
|
||||
"copy:client:fonts",
|
||||
),
|
||||
);
|
||||
|
||||
gulp.task("default", gulp.task("build"));
|
||||
|
||||
gulp.task("watch", () => {
|
||||
gulp.watch(
|
||||
["./packages/*/src/**/*"],
|
||||
{ ignoreInitial: false },
|
||||
gulp.task("build"),
|
||||
);
|
||||
});
|
|
@ -822,7 +822,6 @@ auto: "تلقائي"
|
|||
themeColor: "لون السمة"
|
||||
size: "الحجم"
|
||||
numberOfColumn: "عدد الأعمدة"
|
||||
searchByGoogle: "غوغل"
|
||||
mutePeriod: "مدة الكتم"
|
||||
indefinitely: "أبدًا"
|
||||
tenMinutes: "10 دقائق"
|
||||
|
@ -894,9 +893,6 @@ _aboutFirefish:
|
|||
source: "الشفرة المصدرية"
|
||||
translation: "ترجم ميسكي"
|
||||
donate: "تبرع لميسكي"
|
||||
morePatrons: "نحن نقدر الدعم الذي قدمه العديد من الأشخاص الذين لم نذكرهم. شكرًا
|
||||
لكم 🥰"
|
||||
patrons: "الداعمون"
|
||||
_nsfw:
|
||||
respect: "اخف الوسائط ذات المحتوى الحساس"
|
||||
ignore: "اعرض الوسائط ذات المحتوى الحساس"
|
||||
|
|
|
@ -19,10 +19,10 @@ deleteAndEditConfirm: Сигурни ли сте, че искате да изт
|
|||
copyUsername: Копиране на потребителското име
|
||||
searchUser: Търсене на потребител
|
||||
reply: Отговор
|
||||
showMore: Покажи още
|
||||
showMore: Показване на повече
|
||||
loadMore: Зареди още
|
||||
followRequestAccepted: Заявка за последване приета
|
||||
importAndExport: Импорт/Експорт на Данни
|
||||
followRequestAccepted: Заявката за последване е приета
|
||||
importAndExport: Импорт/експорт на данни
|
||||
import: Импортиране
|
||||
download: Изтегляне
|
||||
export: Експортиране
|
||||
|
@ -34,7 +34,6 @@ searchWith: 'Търсене: {q}'
|
|||
smtpUser: Потребителско име
|
||||
notificationType: Тип известие
|
||||
searchResult: Резултати от търсенето
|
||||
searchByGoogle: Търсене
|
||||
markAsReadAllNotifications: Маркиране на всички известия като прочетени
|
||||
settingGuide: Препоръчителни настройки
|
||||
smtpPass: Парола
|
||||
|
@ -47,7 +46,7 @@ groups: Групи
|
|||
incorrectPassword: Грешна парола.
|
||||
leaveGroup: Напускане на групата
|
||||
numberOfColumn: Брой колони
|
||||
passwordLessLogin: Вход без парола
|
||||
passwordLessLogin: Влизане без парола
|
||||
newPasswordRetype: Повтори новата парола
|
||||
saveAs: Запазване като...
|
||||
resetPassword: Нулиране на паролата
|
||||
|
@ -56,13 +55,13 @@ inputNewFolderName: Въведи ново име на папка
|
|||
upload: Качване
|
||||
retypedNotMatch: Въвежданията не съвпадат.
|
||||
_ago:
|
||||
weeksAgo: преди {n}сед
|
||||
secondsAgo: преди {n}сек
|
||||
hoursAgo: преди {n}ч
|
||||
minutesAgo: преди {n}мин
|
||||
daysAgo: преди {n}д
|
||||
monthsAgo: преди {n}мес
|
||||
yearsAgo: преди {n}г
|
||||
weeksAgo: пр. {n}сед
|
||||
secondsAgo: пр. {n}сек
|
||||
hoursAgo: пр. {n}ч
|
||||
minutesAgo: пр. {n}мин
|
||||
daysAgo: пр. {n}д
|
||||
monthsAgo: пр. {n}мес
|
||||
yearsAgo: пр. {n}г
|
||||
future: Бъдеще
|
||||
justNow: Току-що
|
||||
folderName: Име на папка
|
||||
|
@ -70,7 +69,7 @@ renameFile: Преименуване на файла
|
|||
_widgets:
|
||||
activity: Дейност
|
||||
notifications: Известия
|
||||
timeline: Инфопоток
|
||||
timeline: Хронология
|
||||
clock: Часовник
|
||||
trends: Актуални
|
||||
photos: Снимки
|
||||
|
@ -101,6 +100,8 @@ _profile:
|
|||
metadataLabel: Етикет
|
||||
metadataEdit: Редактиране на допълнителната информация
|
||||
changeAvatar: Промяна на профилната снимка
|
||||
youCanIncludeHashtags: Можеш също да включиш хаштагове в биографията си.
|
||||
changeBanner: Промяна на банера
|
||||
addAccount: Добавяне на акаунт
|
||||
followRequestPending: Заявка за последване в изчакване
|
||||
signinHistory: История на вписванията
|
||||
|
@ -108,7 +109,7 @@ or: Или
|
|||
noUsers: Няма потребители
|
||||
notes: Публикации
|
||||
newNoteRecived: Има нови публикации
|
||||
note: Публикуване
|
||||
note: Публикация
|
||||
instanceFollowing: Последвани на сървъра
|
||||
_filters:
|
||||
followersOnly: Само последователи
|
||||
|
@ -170,7 +171,7 @@ renotesCount: Брой изпратени подсилвания
|
|||
license: Лиценз
|
||||
lastUsedDate: Последно използвано на
|
||||
rename: Преименуване
|
||||
customEmojis: Персонализирани емоджита
|
||||
customEmojis: Персон. емоджита
|
||||
emoji: Емоджи
|
||||
_aboutFirefish:
|
||||
translation: Преведи Firefish
|
||||
|
@ -186,7 +187,7 @@ notesAndReplies: Публикации и отговори
|
|||
noSuchUser: Потребителят не е намерен
|
||||
pinnedPages: Закачени страници
|
||||
pinLimitExceeded: Не може да закачаш повече публикации
|
||||
flagShowTimelineReplies: Показване на отговори в инфопотока
|
||||
flagShowTimelineReplies: Показване на отговори в хронологията
|
||||
followersCount: Брой последователи
|
||||
receivedReactionsCount: Брой получени реакции
|
||||
federation: Федерация
|
||||
|
@ -237,6 +238,7 @@ _theme:
|
|||
installedThemes: Инсталирани теми
|
||||
constant: Константа
|
||||
addConstant: Добавяне на константа
|
||||
make: Направа на тема
|
||||
_pages:
|
||||
script:
|
||||
blocks:
|
||||
|
@ -334,11 +336,15 @@ _pages:
|
|||
title: Заглавие
|
||||
my: Моите страници
|
||||
pageSetting: Настройки на страницата
|
||||
url: Адрес на страницата
|
||||
summary: Кратко обобщение
|
||||
alignCenter: Центриране на елементите
|
||||
variables: Променливи
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: Известия
|
||||
mentions: Споменавания
|
||||
tl: Инфопоток
|
||||
tl: Хронология
|
||||
direct: Директни съобщения
|
||||
list: Списък
|
||||
antenna: Антена
|
||||
|
@ -369,11 +375,11 @@ enterUsername: Въведи потребителско име
|
|||
renotedBy: Подсилено от {user}
|
||||
noNotifications: Няма известия
|
||||
instance: Сървър
|
||||
basicSettings: Основни Настройки
|
||||
otherSettings: Други Настройки
|
||||
basicSettings: Основни настройки
|
||||
otherSettings: Други настройки
|
||||
openInWindow: Отваряне в прозорец
|
||||
profile: Профил
|
||||
timeline: Инфопоток
|
||||
timeline: Хронология
|
||||
noAccountDescription: Този потребител все още не е написал своята биография.
|
||||
login: Вход
|
||||
loggingIn: Вписване
|
||||
|
@ -396,7 +402,7 @@ sendMessage: Изпращане на съобщение
|
|||
jumpToPrevious: Премини към предишно
|
||||
newer: по-ново
|
||||
older: по-старо
|
||||
showLess: Покажи по-малко
|
||||
showLess: Показване на по-малко
|
||||
youGotNewFollower: те последва
|
||||
receiveFollowRequest: Заявка за последване получена
|
||||
mention: Споменаване
|
||||
|
@ -408,7 +414,7 @@ following: Последвани
|
|||
followsYou: Следва те
|
||||
createList: Създаване на списък
|
||||
error: Грешка
|
||||
manageLists: Управление на списъци
|
||||
manageLists: Управление на списъците
|
||||
retry: Повторен опит
|
||||
follow: Последване
|
||||
followRequest: Заявка за последване
|
||||
|
@ -422,7 +428,7 @@ enterEmoji: Въведи емоджи
|
|||
sensitive: Деликатно
|
||||
add: Добавяне
|
||||
pinned: Закачено в профила
|
||||
quote: Цитиране
|
||||
quote: Цитат
|
||||
pinnedNote: Закачена публикация
|
||||
cantReRenote: Подсилване не може да бъде подсилено.
|
||||
clickToShow: Щракни за показване
|
||||
|
@ -555,13 +561,13 @@ _visibility:
|
|||
followers: Последователи
|
||||
specified: Директна
|
||||
localOnly: Само местни
|
||||
public: Публична
|
||||
publicDescription: Публикацията ще бъде видима във всички публични инфопотоци
|
||||
public: Общодостъпна
|
||||
publicDescription: Публикацията ще бъде видима във всички публични хронологии
|
||||
home: Скрита
|
||||
localOnlyDescription: Не е видима за отдалечени потребители
|
||||
specifiedDescription: Видима само за определени потребители
|
||||
followersDescription: Видима само за последователите ти и споменатите потребители
|
||||
homeDescription: Публикуване само в началния инфопоток
|
||||
homeDescription: Публикуване само в началната хронология
|
||||
explore: Разглеждане
|
||||
theme: Теми
|
||||
wallpaper: Тапет
|
||||
|
@ -592,21 +598,21 @@ _tutorial:
|
|||
да разберат дали искат да видят вашите публикации или да ви следват.
|
||||
title: Как се използва Firefish
|
||||
step1_1: Добре дошли!
|
||||
step5_1: Инфопотоци, инфопотоци навсякъде!
|
||||
step5_1: Хронологии, хронологии навсякъде!
|
||||
step3_1: Сега е време да последвате няколко хора!
|
||||
step1_2: Нека да ви настроим. Ще бъдете готови за нула време!
|
||||
step5_3: Началният {icon} инфопоток е мястото, където можете да видите публикации
|
||||
step5_3: Началната {icon} хронология е мястото, където можете да видите публикации
|
||||
от акаунтите, които следвате.
|
||||
step6_1: И така, какво е това място?
|
||||
step5_7: Глобалният {icon} инфопоток е мястото, където можете да видите публикации
|
||||
step5_7: Глобалната {icon} хронология е мястото, където можете да видите публикации
|
||||
от всеки друг свързан сървър.
|
||||
step4_2: За първата си публикация някои хора обичат да правят публикация {introduction}
|
||||
или просто „Здравей свят!“
|
||||
step5_2: Вашият сървър има активирани {timelines} различни инфопотоци.
|
||||
step5_4: Местният {icon} инфопоток е мястото, където можете да видите публикации
|
||||
step5_2: Вашият сървър има активирани {timelines} различни хронологии.
|
||||
step5_4: Местната {icon} хронология е мястото, където можете да видите публикации
|
||||
от всички останали на този сървър.
|
||||
step5_5: Социалният {icon} инфопоток е комбинация от Началния и Местния инфопоток.
|
||||
step5_6: Препоръчаният {icon} инфопоток е мястото, където можете да видите публикации
|
||||
step5_5: Социалната {icon} хронология е комбинация от Началната и Местната хронология.
|
||||
step5_6: Препоръчаната {icon} хронология е мястото, където можете да видите публикации
|
||||
от сървъри, препоръчани от администраторите.
|
||||
step6_4: Сега отидете, изследвайте и се забавлявайте!
|
||||
step6_3: Всеки сървър работи по различни начини и не всички сървъри работят с Firefish.
|
||||
|
@ -638,7 +644,7 @@ _preferencesBackups:
|
|||
updatedAt: 'Обновено на: {date} {time}'
|
||||
editWidgetsExit: Готово
|
||||
done: Готово
|
||||
emailRequiredForSignup: Изискване на адрес на ел. поща за регистриране
|
||||
emailRequiredForSignup: Изискване на адрес за ел. поща за регистриране
|
||||
preview: Преглед
|
||||
privacy: Поверителност
|
||||
about: Относно
|
||||
|
@ -665,14 +671,14 @@ imageUrl: URL адрес на изображение
|
|||
announcements: Оповестявания
|
||||
removeAreYouSure: Сигурни ли сте, че искате да премахнете "{x}"?
|
||||
fromUrl: От URL адрес
|
||||
manageGroups: Управление на групи
|
||||
manageGroups: Управление на групите
|
||||
nUsersRead: прочетено от {n}
|
||||
home: Начало
|
||||
registeredDate: Присъединяване
|
||||
avatar: Профилна снимка
|
||||
watch: Наблюдаване
|
||||
antennas: Антени
|
||||
manageAntennas: Управление на антени
|
||||
manageAntennas: Управление на антените
|
||||
popularTags: Популярни тагове
|
||||
cacheClear: Изчистване на кеша
|
||||
groupName: Име на групата
|
||||
|
@ -700,7 +706,7 @@ gallery: Галерия
|
|||
priority: Приоритет
|
||||
unread: Непрочетени
|
||||
filter: Филтриране
|
||||
manageAccounts: Управление на акаунти
|
||||
manageAccounts: Управление на акаунтите
|
||||
deleteAccount: Изтриване на акаунта
|
||||
fast: Бърза
|
||||
remoteOnly: Само отдалечени
|
||||
|
@ -752,7 +758,7 @@ _feeds:
|
|||
general: Общи
|
||||
metadata: Метаданни
|
||||
disk: Диск
|
||||
featured: Представени
|
||||
featured: Препоръчано
|
||||
yearsOld: на {age} години
|
||||
reload: Опресняване
|
||||
invites: Покани
|
||||
|
@ -776,8 +782,8 @@ uploadFromUrl: Качване от URL адрес
|
|||
instanceName: Име на сървъра
|
||||
instanceDescription: Описание на сървъра
|
||||
accept: Приемане
|
||||
enableLocalTimeline: Включване на местния инфопоток
|
||||
enableGlobalTimeline: Включване на глобалния инфопоток
|
||||
enableLocalTimeline: Включване на местната хронология
|
||||
enableGlobalTimeline: Включване на глобалната хронология
|
||||
removeMember: Премахване на член
|
||||
isAdmin: Администратор
|
||||
isModerator: Модератор
|
||||
|
@ -785,6 +791,9 @@ _menuDisplay:
|
|||
hide: Скриване
|
||||
_exportOrImport:
|
||||
allNotes: Всички публикации
|
||||
followingList: Следвани потребители
|
||||
blockingList: Блокирани потребители
|
||||
muteList: Заглушени потребители
|
||||
exploreFediverse: Разглеждане на Федивселената
|
||||
recentlyUpdatedUsers: Последно активни потребители
|
||||
uiLanguage: Език на потребителския интерфейс
|
||||
|
@ -793,7 +802,7 @@ tags: Тагове
|
|||
youHaveNoGroups: Нямаш групи
|
||||
accessibility: Достъпност
|
||||
email: Ел. поща
|
||||
emailAddress: Адрес на ел. поща
|
||||
emailAddress: Адрес за ел. поща
|
||||
addItem: Добавяне на елемент
|
||||
visibility: Видимост
|
||||
description: Описание
|
||||
|
@ -857,8 +866,8 @@ apply: Прилагане
|
|||
selectAccount: Избор на акаунт
|
||||
muteThread: Заглушаване на нишката
|
||||
ffVisibility: Видимост на Последвани/Последователи
|
||||
renoteMute: Заглушаване на подсилванията в инфопотоците
|
||||
replyMute: Заглушаване на отговорите в инфопотоците
|
||||
renoteMute: Заглуш. на подсилванията в хронолог.
|
||||
replyMute: Заглуш. на отговорите в хронолог.
|
||||
blockConfirm: Сигурни ли сте, че искате да блокирате този акаунт?
|
||||
appearance: Облик
|
||||
fontSize: Размер на шрифта
|
||||
|
@ -867,3 +876,79 @@ unblockConfirm: Сигурни ли сте, че искате да отблок
|
|||
followConfirm: Сигурни ли сте, че искате да последвате {name}?
|
||||
accountMoved: 'Потребителят се премести на нов акаунт:'
|
||||
inputNewDescription: Въведете ново описание
|
||||
tos: Условия за ползване
|
||||
agreeTo: Съгласен съм с {0}
|
||||
withFileAntenna: Само публикации с файлове
|
||||
updateRemoteUser: Обновяване на инфо. за отдалечения потребител
|
||||
receiveAnnouncementFromInstance: Получаване на известия от този сървър
|
||||
userPagePinTip: Можеш да показваш публикации тук, като избереш "Закачане в профила"
|
||||
от менюто на отделните публикации.
|
||||
_ffVisibility:
|
||||
public: Общодостъпна
|
||||
private: Частна
|
||||
followers: Видима само за последователи
|
||||
_charts:
|
||||
activeUsers: Дейни потребители
|
||||
edit: Редактиране
|
||||
toReply: Отговаряне
|
||||
toPost: Публикуване
|
||||
toQuote: Цитиране
|
||||
charts: Диаграми
|
||||
disablePagesScript: Изключване на AiScript в Страниците
|
||||
updatedAt: Обновено на
|
||||
privateDescription: Видима само за теб
|
||||
enableTimelineStreaming: Автоматично обновяване на хронологиите
|
||||
toEdit: Редактиране
|
||||
showEmojisInReactionNotifications: Показване на емоджита в известията за реакции
|
||||
rememberNoteVisibility: Запомняне на настройките за видимост на публикациите
|
||||
drive: Диск
|
||||
banner: Банер
|
||||
public: Общодостъпна
|
||||
makeExplorable: Акаунтът да е видим в "Разглеждане"
|
||||
hideOnlineStatus: Скриване на онлайн състоянието
|
||||
customCss: Персонализиран CSS
|
||||
keepCw: Запазване на предупрежденията за съдържание
|
||||
makeReactionsPublic: Историята на реакциите да е общодостъпна
|
||||
noEmailServerWarning: Сървърът за ел. поща не е конфигуриран.
|
||||
languageForTranslation: Език за превеждане на публикации
|
||||
private: Частна
|
||||
replies: Отговори
|
||||
wordMute: Заглушаване на думи и езици
|
||||
instanceMute: Заглушаване на сървъри
|
||||
notificationSettingDesc: Избиране на какви известия да се показват.
|
||||
preventAiLearning: Предотвратяване на ИИ scraping
|
||||
indexable: Индексируем
|
||||
showPreviewByDefault: Показване на преглед при публикуване по подразбиране
|
||||
showNoAltTextWarning: Показване на предупреждение при опит за публикуване на файлове
|
||||
без описание
|
||||
makeFollowManuallyApprove: Заявките за последване да изискват одобряване
|
||||
enableEmojiReactions: Включване на реакциите с емоджи
|
||||
autoAcceptFollowed: Автоматично одобряване на заявките за последване от последвани
|
||||
потребители
|
||||
expandOnNoteClick: Отваряне на публикацията при кликване
|
||||
enableInfiniteScroll: Автоматично зареждане на повече
|
||||
noCrawle: Предотвратяване на индексирането от търсачки
|
||||
misskeyUpdated: Firefish бе обновен!
|
||||
emailNotConfiguredWarning: Адресът за ел. поща не е зададен.
|
||||
notificationSetting: Настройки за известията
|
||||
emailNotification: Известия по ел. поща
|
||||
clientSettings: Настройки за устройството
|
||||
behavior: Поведение
|
||||
detectPostLanguage: Автоматично откриване на езика и показване на бутон за превеждане
|
||||
за публикации на чужди езици
|
||||
replyUnmute: Отмяна на заглушаването на отговорите
|
||||
searchWords: Думи за търсене / ID или URL за поглеждане
|
||||
reloadConfirm: Искате ли да опресните хронологията?
|
||||
enableRecommendedTimeline: Включване на препоръчаната хронология
|
||||
showGapBetweenNotesInTimeline: Показване на празнина между публикациите в хронологията
|
||||
lookup: Поглеждане
|
||||
media: Мултимедия
|
||||
welcomeBackWithName: Добре дошли отново, {name}
|
||||
reduceUiAnimation: Намаляване на UI анимациите
|
||||
clickToFinishEmailVerification: Моля, натиснете [{ok}], за да завършите потвърждаването
|
||||
на ел. поща.
|
||||
_cw:
|
||||
show: Показване на съдържанието
|
||||
remoteFollow: Отдалечено последване
|
||||
messagingUnencryptedInfo: Чатовете във Firefish не са шифровани от край до край. Не
|
||||
споделяйте чувствителна информация през Firefish.
|
||||
|
|
|
@ -894,7 +894,6 @@ auto: "স্বয়ংক্রিয়"
|
|||
themeColor: "থিমের রং"
|
||||
size: "আকার"
|
||||
numberOfColumn: "কলামের সংখ্যা"
|
||||
searchByGoogle: "গুগল"
|
||||
instanceDefaultLightTheme: "ইন্সট্যান্সের ডিফল্ট লাইট থিম"
|
||||
instanceDefaultDarkTheme: "ইন্সট্যান্সের ডিফল্ট ডার্ক থিম"
|
||||
instanceDefaultThemeDescription: "অবজেক্ট ফরম্যাটে থিম কোড লিখুন"
|
||||
|
@ -976,8 +975,6 @@ _aboutFirefish:
|
|||
source: "সোর্স কোড"
|
||||
translation: "Firefish অনুবাদ করুন"
|
||||
donate: "Firefish তে দান করুন"
|
||||
morePatrons: "আরও অনেকে আমাদের সাহায্য করছেন। তাদের সবাইকে ধন্যবাদ 🥰"
|
||||
patrons: "সমর্থনকারী"
|
||||
_nsfw:
|
||||
respect: "স্পর্শকাতর মিডিয়া লুকান"
|
||||
ignore: "স্পর্শকাতর মিডিয়া লুকাবেন না"
|
||||
|
|
|
@ -9,14 +9,14 @@ notifications: "Notificacions"
|
|||
username: "Nom d'usuari"
|
||||
password: "Contrasenya"
|
||||
forgotPassword: "Contrasenya oblidada"
|
||||
fetchingAsApObject: "Cercant en el Fediverse"
|
||||
fetchingAsApObject: "Obtenint des de el Fediverse"
|
||||
ok: "D'acord"
|
||||
gotIt: "Ho he entès!"
|
||||
cancel: "Cancel·la"
|
||||
enterUsername: "Introdueix el teu nom d'usuari"
|
||||
renotedBy: "Impulsat per {user}"
|
||||
noNotes: "Cap publicació"
|
||||
noNotifications: "Cap notificació"
|
||||
noNotes: "Sense publicacions"
|
||||
noNotifications: "Sense notificacions"
|
||||
instance: "Servidor"
|
||||
settings: "Preferències"
|
||||
basicSettings: "Configuració bàsica"
|
||||
|
@ -35,23 +35,23 @@ users: "Usuaris"
|
|||
addUser: "Afegeix un usuari"
|
||||
favorite: "Afegeix a les adreces d'interès"
|
||||
favorites: "Adreces d'interès"
|
||||
unfavorite: "Eliminar de les adreces d'interès"
|
||||
favorited: "Afegit a les adreces d'interès."
|
||||
alreadyFavorited: "Ja es troba a les adreces d'interès."
|
||||
unfavorite: "Suprimeix de les adreces d'interès"
|
||||
favorited: "S'ha afegit a les adreces d'interès."
|
||||
alreadyFavorited: "Ja s'ha afegit a les adreces d'interès."
|
||||
cantFavorite: "No s'ha pogut afegir a les adreces d'interès."
|
||||
pin: "Fixar al perfil"
|
||||
unpin: "Deixa de fixar al perfil"
|
||||
pin: "Fixa al perfil"
|
||||
unpin: "No fixis al perfil"
|
||||
copyContent: "Copia el contingut"
|
||||
copyLink: "Copia l'enllaç"
|
||||
delete: "Elimina"
|
||||
deleteAndEdit: "Elimina i edita"
|
||||
deleteAndEditConfirm: "Segur que vols eliminar la publicació i editar-la? Perdràs
|
||||
delete: "Suprimeix"
|
||||
deleteAndEdit: "Suprimeix i edita"
|
||||
deleteAndEditConfirm: "Segur que vols suprimir aquesta publicació i editar-la? Perdràs
|
||||
totes les reaccions, impulsos i respostes."
|
||||
addToList: "Afegeix a la llista"
|
||||
sendMessage: "Envia un missatge"
|
||||
copyUsername: "Copia el nom d'usuari"
|
||||
searchUser: "Cerca un usuari"
|
||||
reply: "Respon"
|
||||
reply: "Resposta"
|
||||
loadMore: "Carrega'n més"
|
||||
showMore: "Mostra'n més"
|
||||
youGotNewFollower: "t'ha seguit"
|
||||
|
@ -66,20 +66,20 @@ export: "Exporta"
|
|||
files: "Fitxers"
|
||||
download: "Baixa"
|
||||
driveFileDeleteConfirm: "Segur que vols eliminar el fitxer «{name}»? S'eliminarà de
|
||||
totes les notes que el continguin com a fitxer adjunt."
|
||||
totes les publicacions que el continguin com a fitxer adjunt."
|
||||
unfollowConfirm: "Segur que vols deixar de seguir a {name}?"
|
||||
exportRequested: "Has demanat exportar dades. Això pot trigar una estona. S'afegirà
|
||||
al teu Disc un cop completada."
|
||||
importRequested: "Has demanat importar dades. Això pot trigar una estona."
|
||||
exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà
|
||||
al teu Disc un cop finalitzada."
|
||||
importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona."
|
||||
lists: "Llistes"
|
||||
noLists: "No tens cap llista"
|
||||
note: "Publica"
|
||||
note: "Publicació"
|
||||
notes: "Publicacions"
|
||||
following: "Seguint"
|
||||
followers: "Seguidors"
|
||||
followsYou: "Et segueix"
|
||||
createList: "Crear una llista"
|
||||
manageLists: "Gestionar les llistes"
|
||||
createList: "Crea una llista"
|
||||
manageLists: "Gestiona les llistes"
|
||||
error: "Error"
|
||||
somethingHappened: "S'ha produït un error"
|
||||
retry: "Torna-ho a intentar"
|
||||
|
@ -91,7 +91,7 @@ serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar.
|
|||
youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar
|
||||
el vostre client."
|
||||
enterListName: "Introdueix un nom per a la llista"
|
||||
privacy: "Privadesa"
|
||||
privacy: "Privacitat"
|
||||
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
|
||||
defaultNoteVisibility: "Visibilitat per defecte"
|
||||
follow: "Segueix"
|
||||
|
@ -135,7 +135,6 @@ userList: "Llistes"
|
|||
smtpUser: "Nom d'usuari"
|
||||
smtpPass: "Contrasenya"
|
||||
user: "Usuari"
|
||||
searchByGoogle: "Cerca"
|
||||
file: "Fitxer"
|
||||
_email:
|
||||
_follow:
|
||||
|
@ -739,6 +738,7 @@ _notification:
|
|||
reacted: Ha reaccionat a la teva publicació
|
||||
renoted: Ha impulsat la teva publicació
|
||||
voted: Ha votat a la teva enquesta
|
||||
andCountUsers: I {count} usuaris més {acted}
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: "Notificacions"
|
||||
|
@ -856,8 +856,8 @@ more: Més!
|
|||
featured: Destacat
|
||||
usernameOrUserId: Nom o ID d'usuari
|
||||
noSuchUser: No s'ha trobat l'usuari
|
||||
lookup: Cerca
|
||||
attachFile: Afegeix un fitxer
|
||||
lookup: Ves a
|
||||
attachFile: Afegeix fitxers
|
||||
currentPassword: Contrasenya actual
|
||||
newPassword: Nova contrasenya
|
||||
announcements: Avisos
|
||||
|
@ -877,7 +877,7 @@ fromDrive: Des del Disc
|
|||
uploadFromUrl: Puja des d'una adreça URL
|
||||
uploadFromUrlDescription: Adreça URL del fitxer que vols pujar
|
||||
uploadFromUrlRequested: Pujada demanada
|
||||
noMoreHistory: No hi ha res més a l'historial
|
||||
noMoreHistory: No hi ha més historial
|
||||
tos: Condicions d'ús
|
||||
start: Comença
|
||||
startMessaging: Comença una conversa
|
||||
|
@ -1577,7 +1577,7 @@ cannotUploadBecauseInappropriate: Aquest fitxer no s'ha pogut carregar perquè s
|
|||
cannotUploadBecauseNoFreeSpace: La pujada ha fallat a causa de la manca d'espai al
|
||||
Disc.
|
||||
enableAutoSensitive: Marcatge automàtic NSFW
|
||||
moveTo: Mou el compte actual al compte nou
|
||||
moveTo: Mou aquest compte a un compte nou
|
||||
customKaTeXMacro: Macros KaTeX personalitzats
|
||||
_aboutFirefish:
|
||||
contributors: Col·laboradors principals
|
||||
|
@ -1587,18 +1587,12 @@ _aboutFirefish:
|
|||
translation: Tradueix Firefish
|
||||
about: Firefish és una bifurcació de Misskey feta per ThatOneCalculator, que està
|
||||
en desenvolupament des del 2022.
|
||||
morePatrons: També agraïm el suport de molts altres ajudants que no figuren aquí.
|
||||
Gràcies! 🥰
|
||||
patrons: Mecenes de Firefish
|
||||
patronsList: Llistats cronològicament, no per la quantitat donada. Fes una donació
|
||||
amb l'enllaç de dalt per veure el teu nom aquí!
|
||||
donateTitle: T'agrada Firefish?
|
||||
pleaseDonateToFirefish: Penseu en fer una donació a Firefish per donar suport al
|
||||
seu desenvolupament.
|
||||
pleaseDonateToHost: Penseu també en fer una donació a la vostre instància, {host},
|
||||
per ajudar-lo a suportar els costos de funcionament.
|
||||
donateHost: Fes una donació a {host}
|
||||
sponsors: Patrocinadors de Calckey
|
||||
misskeyContributors: Col·laboradors de Misskey
|
||||
unknown: Desconegut
|
||||
pageLikesCount: Nombre de pàgines amb M'agrada
|
||||
|
@ -1724,7 +1718,7 @@ deleteConfirm: De veritat ho vols esborrar?
|
|||
receiveAnnouncementFromInstance: Rep notificacions d'aquest servidor
|
||||
emailNotification: Notificacions per correu electrònic
|
||||
publish: Publicar
|
||||
inChannelSearch: Buscar al canal
|
||||
inChannelSearch: Cerca al canal
|
||||
useReactionPickerForContextMenu: Obrir el selector de reaccions al fer click esquerra
|
||||
typingUsers: L'{users} està escrivint
|
||||
oneDay: Un dia
|
||||
|
@ -1762,7 +1756,7 @@ customSplashIconsDescription: Les URLS de les icones personalitzades a la pantal
|
|||
de benvinguda separades per salts de línia. Es mostraran aleatòriament cada vegada
|
||||
que un usuari carrega/recarrega la pàgina. Si us plau, assegureu-vos que les imatges
|
||||
estiguin en una URL estàtica, preferiblement amb imatges amb la de 192 x 192.
|
||||
moveFrom: Mou a aquest compte des d'un compte anterior
|
||||
moveFrom: Mou-te a aquest compte des d'un compte anterior
|
||||
moveFromLabel: 'Compte des del qual us moveu:'
|
||||
migrationConfirm: "Esteu absolutament segur que voleu migrar el vostre compte a {account}?
|
||||
Un cop ho feu, no podreu revertir-ho i no podreu tornar a utilitzar el vostre compte
|
||||
|
@ -2066,7 +2060,7 @@ _relayStatus:
|
|||
rejected: Rebutjat
|
||||
deleted: Eliminat
|
||||
editNote: Edita la publicació
|
||||
edited: 'Editat el {date} {time}'
|
||||
edited: 'Editat a les {time} {date}'
|
||||
signupsDisabled: Actualment, les inscripcions en aquest servidor estan desactivades.
|
||||
Si teniu un codi d'invitació per a aquest servidor, introduïu-lo a continuació.
|
||||
userSaysSomethingReasonQuote: '{name} ha citat una publicació que conté {reason}'
|
||||
|
@ -2085,9 +2079,9 @@ _experiments:
|
|||
release: Publicà
|
||||
title: Experiments
|
||||
enablePostImports: Activar l'importació de publicacions
|
||||
postImportsCaption: Permet els usuaris importar publicacions desde comptes a Firefish,
|
||||
postImportsCaption: Permet als usuaris importar publicacions des de comptes de Firefish,
|
||||
Misskey, Mastodon, Akkoma i Pleroma. Pot fer que el servidor vagi més lent durant
|
||||
la càrrega si tens un coll d'ampolla a la cua.
|
||||
la importació si la teva cua de feina és saturada.
|
||||
noGraze: Si us plau, desactiva l'extensió del navegador "Graze for Mastodon", ja que
|
||||
interfereix amb Firefish.
|
||||
accessibility: Accessibilitat
|
||||
|
@ -2096,7 +2090,7 @@ newer: Més nou
|
|||
older: Més antic
|
||||
silencedWarning: S'està mostrant aquesta pàgina per què aquest usuari és d'un servidor
|
||||
que l'administrador a silenciat, així que pot ser spam.
|
||||
jumpToPrevious: Vés a l'anterior
|
||||
jumpToPrevious: Salta a l'anterior
|
||||
cw: Avís de contingut
|
||||
antennasDesc: "Les antenes mostren publicacions noves que coincideixen amb els criteris
|
||||
establerts!\nS'hi pot accedir des de la pàgina de línies de temps."
|
||||
|
@ -2104,7 +2098,7 @@ expandOnNoteClick: Obre la publicació amb un clic
|
|||
expandOnNoteClickDesc: Si està desactivat, encara pots obrir les publicacions al menú
|
||||
del botó dret o fent clic a la marca de temps.
|
||||
channelFederationWarn: Els canals encara no es federen amb altres servidors
|
||||
searchPlaceholder: Cerca a Firefish
|
||||
searchPlaceholder: Cercar a Firefish
|
||||
listsDesc: Les llistes et permeten crear línies de temps amb usuaris específics. Es
|
||||
pot accedir des de la pàgina de línies de temps.
|
||||
clipsDesc: Els clips són com marcadors categoritzats que es poden compartir. Podeu
|
||||
|
@ -2210,8 +2204,8 @@ squareCatAvatars: Mostrar avatars quadrats per a comptes de gats
|
|||
replaceChatButtonWithAccountButton: Canviar el botó de xats amb el botó de canvi de
|
||||
compte
|
||||
replaceWidgetsButtonWithReloadButton: Canviar el botó de ginys amb el botó de recarregar
|
||||
searchEngine: Motor de cerca fet servir anla barra MFM
|
||||
postSearch: Posar els resultats en aquest servidor
|
||||
searchEngine: Motor de cerca fet servir a la barra MFM
|
||||
postSearch: Publicar els resultats en aquest servidor
|
||||
showBigPostButton: Mostrar un botó gegant al formulari de publicació
|
||||
_emojiModPerm:
|
||||
unauthorized: Res
|
||||
|
@ -2238,7 +2232,7 @@ enableTimelineStreaming: Actualitza les línies de temps automàticament
|
|||
enablePullToRefresh: Activa "Baixa per actualitzar"
|
||||
pullDownToReload: Baixa per actualitzar
|
||||
pullToRefreshThreshold: Distancia de baixada per actualitzar
|
||||
searchWords: Paraules / ID o adreça a URL a buscar
|
||||
searchWords: Paraules / ID o adreça URL que vols cercar
|
||||
noSentFollowRequests: No tens cap sol·licitud de seguiment enviada
|
||||
sentFollowRequests: Enviar sol·licituds de seguiment
|
||||
replyMute: Silencia les respostes a les línies de temps
|
||||
|
@ -2251,12 +2245,12 @@ searchWordsDescription: "Per cercar publicacions, escriu el terme a buscar. Sepa
|
|||
Si vols cercar per una seqüencia de paraules (per exemple una frase) has d'escriure-les
|
||||
entre cometes dobles, per no fer una cerca amb condicionant AND: \"Avui he aprés\"\
|
||||
\n \nSi vols anar a una pàgina d'usuari o publicació en concret, escriu la adreça
|
||||
URL o la ID en aquest camp i fes clic al botó 'Trobar'. Fent clic a 'Cercar' trobarà
|
||||
URL o la ID en aquest camp i fes clic al botó 'Ves a'. Fent clic a 'Cerca' trobarà
|
||||
publicacions que, literalment , continguin la ID/adreça URL."
|
||||
searchPostsWithFiles: Només publicacions amb fitxers
|
||||
searchCwAndAlt: Inclou avisos de contingut i arxius amb descripcions
|
||||
searchUsers: Publicat per (opcional)
|
||||
searchRange: Publicat dintre de (opcional)
|
||||
searchRange: Publicat entre les dates (opcional)
|
||||
publishTimelines: Publica línies de temps per visitants
|
||||
toPost: Publicar
|
||||
publishTimelinesDescription: Si està activat, les línies de temps Global i Local es
|
||||
|
@ -2264,8 +2258,8 @@ publishTimelinesDescription: Si està activat, les línies de temps Global i Loc
|
|||
noAltTextWarning: Alguns fitxers adjunts no tenen una descripció. T'has s oblidat
|
||||
d'escriure-les?
|
||||
showNoAltTextWarning: Mostra un avís si públiques un fitxer sense descripció
|
||||
toReply: Respon
|
||||
toQuote: Cita
|
||||
toReply: Resposta
|
||||
toQuote: Citar
|
||||
toEdit: Edita
|
||||
searchUsersDescription: "Per buscar publicacions concretes d'un usuari/servidor, escriu
|
||||
la ID (@usuari@exemple.com, o @usuari per un usuari local) o nom del domini (exemple.com).\n
|
||||
|
@ -2285,3 +2279,31 @@ searchRangeDescription: "Si vols filtrar per un període de temps, has de fer se
|
|||
moderationNote: Nota de moderació
|
||||
ipFirstAcknowledged: Data en què es va veure la adreça IP per primera vegada
|
||||
driveCapacityOverride: Capacitat del disc esgotada
|
||||
incorrectLanguageWarning: "Semblar ser que la teva publicació es troba en {detected},
|
||||
però has seleccionat {current}.\nVols canviar l'idioma a {detected}?"
|
||||
markLocalFilesNsfwByDefault: Marcar tots els fitxers locals nous com a sensibles per
|
||||
defecte
|
||||
markLocalFilesNsfwByDefaultDescription: Independentment d'aquest ajust, els usuaris
|
||||
poden treure l'etiqueta NSFW els mateixos. Els fitxers que ja existeixen no es veuen
|
||||
afectats.
|
||||
autocorrectNoteLanguage: Mostra un avís si l'idioma de la publicació no coincideix
|
||||
amb el resultat de l'idioma detectat automàticament
|
||||
noteEditHistory: Historial d'edicions
|
||||
media: Multimèdia
|
||||
antennaLimit: El nombre màxim d'antenes que pot crear un usuari
|
||||
showAddFileDescriptionAtFirstPost: Obra de forma automàtica un formulari per escriure
|
||||
una descripció quant intentes publicar un fitxer que no en té
|
||||
remoteFollow: Seguiment remot
|
||||
cannotEditVisibility: No pots canviar la visibilitat
|
||||
useThisAccountConfirm: Vols continuar amb aquest compte?
|
||||
inputAccountId: Sisplau introdueix el teu compte (per exemple @firefish@info.firefish.dev)
|
||||
getQrCode: Mostrar el codi QR
|
||||
copyRemoteFollowUrl: Còpia la adreça URL del seguidor remot
|
||||
foldNotification: Agrupar les notificacions similars
|
||||
slashQuote: Cita encadenada
|
||||
i18nServerInfo: Els nous clients els trobares en {language} per defecte.
|
||||
i18nServerChange: Fes servir {language} en comptes.
|
||||
i18nServerSet: Fes servir {language} per els nous clients.
|
||||
mergeThreadInTimeline: Fusiona diferents publicacions en un mateix fil a les línies
|
||||
de temps
|
||||
mergeRenotesInTimeline: Agrupa diferents impulsos d'una mateixa publicació
|
||||
|
|
|
@ -600,7 +600,6 @@ tablet: "Tablet"
|
|||
auto: "Auto"
|
||||
size: "Velikost"
|
||||
numberOfColumn: "Počet sloupců"
|
||||
searchByGoogle: "Vyhledávání"
|
||||
indefinitely: "Navždy"
|
||||
tenMinutes: "10 minut"
|
||||
oneHour: "1 hodina"
|
||||
|
|
|
@ -923,7 +923,6 @@ auto: "Automatisch"
|
|||
themeColor: "Farbe der Ticker-Laufschrift"
|
||||
size: "Größe"
|
||||
numberOfColumn: "Spaltenanzahl"
|
||||
searchByGoogle: "Suchen"
|
||||
instanceDefaultLightTheme: "Standard-Farbkombination auf diesem Server: \"Hell\""
|
||||
instanceDefaultDarkTheme: "Standard-Farbkombination auf diesem Server: \"Dunkel\""
|
||||
instanceDefaultThemeDescription: "Gib den Farbschemencode im Objektformat ein."
|
||||
|
@ -1093,17 +1092,11 @@ _aboutFirefish:
|
|||
source: "Quellcode"
|
||||
translation: "Firefish übersetzen"
|
||||
donate: "An Firefish spenden"
|
||||
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter
|
||||
Personen sehr. Danke! 🥰"
|
||||
patrons: "UnterstützerInnen"
|
||||
patronsList: Auflistung chonologisch, nicht nach Spenden-Größe. Spende über den
|
||||
Link oben, um hier aufgeführt zu werden!
|
||||
donateTitle: Gefällt dir Firefish?
|
||||
pleaseDonateToFirefish: Bitte erwäge eine Spende an Firefish, um dessen Entwicklung
|
||||
zu unterstützen.
|
||||
pleaseDonateToHost: Bitte erwäge auch, an deinen Heimatserver {host} zu spenden,
|
||||
um bei der Deckung der Betriebskosten zu helfen.
|
||||
sponsors: Firefish-Sponsoren
|
||||
donateHost: Spende an {host}
|
||||
misskeyContributors: Misskey-Mitwirkende
|
||||
_nsfw:
|
||||
|
|
|
@ -295,7 +295,6 @@ searchResult: "Αποτελέσματα αναζήτησης"
|
|||
learnMore: "Μάθετε περισσότερα"
|
||||
controlPanel: "Πίνακας ελέγχου"
|
||||
manageAccounts: "Διαχείριση Λογαριασμών"
|
||||
searchByGoogle: "Αναζήτηση"
|
||||
file: "Αρχεία"
|
||||
recommended: "Προτεινόμενα"
|
||||
cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω ανεπαρκούς Αποθηκευτικού
|
||||
|
|
|
@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration"
|
|||
invite: "Invite"
|
||||
driveCapacityPerLocalAccount: "Drive capacity per local user"
|
||||
driveCapacityPerRemoteAccount: "Drive capacity per remote user"
|
||||
antennaLimit: "The maximum number of antennas that each user can create"
|
||||
inMb: "In megabytes"
|
||||
iconUrl: "Icon URL"
|
||||
bannerUrl: "Banner image URL"
|
||||
|
@ -644,6 +645,7 @@ deletedNote: "Deleted post"
|
|||
invisibleNote: "Invisible post"
|
||||
enableInfiniteScroll: "Automatically load more"
|
||||
visibility: "Visiblility"
|
||||
cannotEditVisibility: "You can't edit the visibility"
|
||||
poll: "Poll"
|
||||
useCw: "Hide content"
|
||||
enablePlayer: "Open video player"
|
||||
|
@ -708,6 +710,7 @@ display: "Display"
|
|||
copy: "Copy"
|
||||
metrics: "Metrics"
|
||||
overview: "Overview"
|
||||
media: "Media"
|
||||
logs: "Logs"
|
||||
delayed: "Delayed"
|
||||
database: "Database"
|
||||
|
@ -763,6 +766,9 @@ confirmToUnclipAlreadyClippedNote: "This post is already part of the \"{name}\"
|
|||
public: "Public"
|
||||
i18nInfo: "Firefish is being translated into various languages by volunteers. You
|
||||
can help at {link}."
|
||||
i18nServerInfo: "New clients will be in {language} by default."
|
||||
i18nServerChange: "Use {language} instead."
|
||||
i18nServerSet: "Use {language} for new clients."
|
||||
manageAccessTokens: "Manage access tokens"
|
||||
accountInfo: "Account Info"
|
||||
notesCount: "Number of posts"
|
||||
|
@ -977,7 +983,6 @@ auto: "Auto"
|
|||
themeColor: "Server Ticker Color"
|
||||
size: "Size"
|
||||
numberOfColumn: "Number of columns"
|
||||
searchByGoogle: "Search"
|
||||
instanceDefaultLightTheme: "Server-wide default light theme"
|
||||
instanceDefaultDarkTheme: "Server-wide default dark theme"
|
||||
instanceDefaultThemeDescription: "Enter the theme code in object format."
|
||||
|
@ -1009,6 +1014,8 @@ isSystemAccount: "This account is created and automatically operated by the syst
|
|||
Please do not moderate, edit, delete, or otherwise tamper with this account, or
|
||||
it may break your server."
|
||||
typeToConfirm: "Please enter {x} to confirm"
|
||||
useThisAccountConfirm: "Do you want to continue with this account?"
|
||||
inputAccountId: "Please input your account (e.g., @firefish@info.firefish.dev)"
|
||||
deleteAccount: "Delete account"
|
||||
document: "Documentation"
|
||||
numberOfPageCache: "Number of cached pages"
|
||||
|
@ -1155,6 +1162,9 @@ addRe: "Add \"re:\" at the beginning of comment in reply to a post with a conten
|
|||
confirm: "Confirm"
|
||||
importZip: "Import ZIP"
|
||||
exportZip: "Export ZIP"
|
||||
getQrCode: "Show QR code"
|
||||
remoteFollow: "Remote follow"
|
||||
copyRemoteFollowUrl: "Copy remote follow URL"
|
||||
emojiPackCreator: "Emoji pack creator"
|
||||
indexable: "Indexable"
|
||||
indexableDescription: "Allow built-in search to show your public posts"
|
||||
|
@ -1196,16 +1206,16 @@ releaseToReload: "Release to reload"
|
|||
reloading: "Reloading"
|
||||
enableTimelineStreaming: "Update timelines automatically"
|
||||
searchWords: "Words to search / ID or URL to lookup"
|
||||
searchWordsDescription: "Enter the search term here to search for posts. Separate words
|
||||
with a space for an AND search, or 'OR' (without quotes) between words for an OR
|
||||
search.\nFor example, 'morning night' will find posts that contain both 'morning'
|
||||
searchWordsDescription: "Enter the search term here to search for posts. Separate
|
||||
words with a space for an AND search, or 'OR' (without quotes) between words for
|
||||
an OR search.\nFor example, 'morning night' will find posts that contain both 'morning'
|
||||
and 'night', and 'morning OR night' will find posts that contain either 'morning'
|
||||
or 'night' (or both).\nYou can also combine AND/OR conditions like '(morning OR
|
||||
night) sleepy'.\nIf you want to search for a sequence of words (e.g., a sentence), you
|
||||
must put it in double quotes, not to make it an AND search: \"Today I learned\"\n\n
|
||||
If you want to go to a specific user page or post page, enter
|
||||
the ID or URL in this field and click the 'Lookup' button. Clicking 'Search' will
|
||||
search for posts that literally contain the ID/URL."
|
||||
night) sleepy'.\nIf you want to search for a sequence of words (e.g., a sentence),
|
||||
you must put it in double quotes, not to make it an AND search: \"Today I learned\"\
|
||||
\n\n If you want to go to a specific user page or post page, enter the ID or URL
|
||||
in this field and click the 'Lookup' button. Clicking 'Search' will search for posts
|
||||
that literally contain the ID/URL."
|
||||
searchUsers: "Posted by (optional)"
|
||||
searchUsersDescription: "To search for posts by a specific user/server, enter the
|
||||
ID (@user@example.com, or @user for a local user) or domain name (example.com).\n
|
||||
|
@ -1226,6 +1236,8 @@ publishTimelinesDescription: "If enabled, the Local and Global timelines will be
|
|||
on {url} even when signed out."
|
||||
noAltTextWarning: "Some attached file(s) have no description. Did you forget to write?"
|
||||
showNoAltTextWarning: "Show a warning if you attempt to post files without a description"
|
||||
showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description when you
|
||||
attempt to post files without a description"
|
||||
|
||||
_emojiModPerm:
|
||||
unauthorized: "None"
|
||||
|
@ -1336,12 +1348,6 @@ _aboutFirefish:
|
|||
pleaseDonateToHost: "Please also consider donating to your home server, {host},
|
||||
to help support its operation costs."
|
||||
donateHost: "Donate to {host}"
|
||||
morePatrons: "We also appreciate the support of many other helpers not listed here.
|
||||
Thank you! 🥰"
|
||||
sponsors: "Firefish sponsors"
|
||||
patrons: "Firefish patrons"
|
||||
patronsList: "Listed chronologically, not by donation size. Donate with the link
|
||||
above to get your name on here!"
|
||||
_nsfw:
|
||||
respect: "Hide NSFW media"
|
||||
ignore: "Don't hide NSFW media"
|
||||
|
@ -2149,6 +2155,7 @@ _notification:
|
|||
reacted: "reacted to your post"
|
||||
renoted: "boosted your post"
|
||||
voted: "voted on your poll"
|
||||
andCountUsers: "and {count} more users {acted}"
|
||||
_types:
|
||||
all: "All"
|
||||
follow: "New followers"
|
||||
|
@ -2230,5 +2237,12 @@ moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lo
|
|||
left corner using this notation:\n\"Display name\": https://example.com/"
|
||||
messagingUnencryptedInfo: "Chats on Firefish are not end-to-end encrypted. Don't share
|
||||
any sensitive infomation over Firefish."
|
||||
autocorrectNoteLanguage: "Show a warning if the post language does not match the auto-detected result"
|
||||
incorrectLanguageWarning: "It looks like your post is in {detected}, but you selected {current}.\nWould you like to set the language to {detected} instead?"
|
||||
autocorrectNoteLanguage: "Show a warning if the post language does not match the auto-detected
|
||||
result"
|
||||
incorrectLanguageWarning: "It looks like your post is in {detected}, but you selected
|
||||
{current}.\nWould you like to set the language to {detected} instead?"
|
||||
noteEditHistory: "Post edit history"
|
||||
slashQuote: "Chain quote"
|
||||
foldNotification: "Group similar notifications"
|
||||
mergeThreadInTimeline: "Merge multiple posts in the same thread in timelines"
|
||||
mergeRenotesInTimeline: "Group multiple boosts of the same post"
|
||||
|
|
1
locales/eo.yml
Normal file
1
locales/eo.yml
Normal file
|
@ -0,0 +1 @@
|
|||
_lang_: "Esperanto"
|
|
@ -907,7 +907,6 @@ auto: "Automático"
|
|||
themeColor: "Color de la marquesina del servidor"
|
||||
size: "Tamaño"
|
||||
numberOfColumn: "Cantidad de columnas"
|
||||
searchByGoogle: "Buscar"
|
||||
instanceDefaultLightTheme: "Tema claro por defecto del servidor"
|
||||
instanceDefaultDarkTheme: "Tema oscuro por defecto del servidor"
|
||||
instanceDefaultThemeDescription: "Ingrese el código del tema en formato objeto"
|
||||
|
@ -1074,17 +1073,11 @@ _aboutFirefish:
|
|||
source: "Código fuente"
|
||||
translation: "Traducir Firefish"
|
||||
donate: "Donar a Firefish"
|
||||
morePatrons: "También apreciamos el apoyo de muchos más que no están enlistados
|
||||
aquí. ¡Gracias! 🥰"
|
||||
patrons: "Mecenas de Firefish"
|
||||
pleaseDonateToFirefish: Por favor considera donar a Firefish para apollar su desarrollo.
|
||||
donateHost: Dona a {host}
|
||||
patronsList: Listados cronológicamente no por monto de la donación. ¡Dona con el
|
||||
vínculo de arriba para que tu nombre aparezca aquí!
|
||||
donateTitle: ¿Te gusta Firefish?
|
||||
pleaseDonateToHost: También considera donar a tu propio servidor , {host}, para
|
||||
ayudar con los costos de operación.
|
||||
sponsors: Patrocinadores de Firefish
|
||||
misskeyContributors: Contribuidores de Misskey
|
||||
_nsfw:
|
||||
respect: "Ocultar medios NSFW"
|
||||
|
|
|
@ -948,7 +948,6 @@ clickToFinishEmailVerification: Klikkaa [{ok}] viimeistelläksesi sähköpostiva
|
|||
overridedDeviceKind: Laitetyyppi
|
||||
tablet: Tabletti
|
||||
numberOfColumn: Sarakkeiden määrä
|
||||
searchByGoogle: Etsi
|
||||
mutePeriod: Vaiennuksen kesto
|
||||
indefinitely: Pysyvästi
|
||||
tenMinutes: 10 minuuttia
|
||||
|
|
|
@ -910,7 +910,6 @@ auto: "Automatique"
|
|||
themeColor: "Couleur du bandeau d'information du serveur"
|
||||
size: "Taille"
|
||||
numberOfColumn: "Nombre de colonnes"
|
||||
searchByGoogle: "Google"
|
||||
instanceDefaultLightTheme: "Thème clair par défaut sur tout le serveur"
|
||||
instanceDefaultDarkTheme: "Thème sombre par défaut sur tout le serveur"
|
||||
instanceDefaultThemeDescription: "Saisissez le code du thème en format objet."
|
||||
|
@ -929,6 +928,8 @@ colored: "Coloré"
|
|||
label: "Étiquette"
|
||||
localOnly: "Local seulement"
|
||||
account: "Comptes"
|
||||
getQrCode: "Afficher le code QR"
|
||||
|
||||
_emailUnavailable:
|
||||
used: "Adresse non disponible"
|
||||
format: "Le format de cette adresse de courriel est invalide"
|
||||
|
@ -997,18 +998,12 @@ _aboutFirefish:
|
|||
source: "Code source"
|
||||
translation: "Traduire Firefish"
|
||||
donate: "Soutenir Firefish"
|
||||
morePatrons: "Nous apprécions vraiment le soutien de nombreuses autres personnes
|
||||
non mentionnées ici. Merci à toutes et à tous ! 🥰"
|
||||
patrons: "Contributeurs"
|
||||
pleaseDonateToFirefish: Merci de considérer de faire un don pour soutenir le développement
|
||||
de Firefish.
|
||||
sponsors: Sponsors Firefish
|
||||
donateTitle: Firefish vous plaît ?
|
||||
pleaseDonateToHost: Également, veuillez envisager de faire un don à votre serveur
|
||||
d'accueil, {host}, pour contribuer à couvrir ses frais de fonctionnement.
|
||||
donateHost: Faire un don à {host}
|
||||
patronsList: Listé chronologiquement, pas par taille de donation. Faite un don avec
|
||||
le lien ci-dessus pour avoir votre nom affiché ici !
|
||||
misskeyContributors: Contributeurs Misskey
|
||||
_nsfw:
|
||||
respect: "Cacher les médias marqués comme contenu sensible (NSFW)"
|
||||
|
@ -1147,8 +1142,8 @@ _wordMute:
|
|||
mutedNotes: "Publications masquées"
|
||||
muteLangsDescription2: Utiliser les codes de langue (i.e en, fr, ja, zh).
|
||||
lang: Langue
|
||||
langDescription: Cacher du fil de publication les publications qui correspondent
|
||||
à ces langues.
|
||||
langDescription: Cachez les publications qui correspondent à la langue définie dans
|
||||
le fil d'actualité.
|
||||
muteLangs: Langues filtrées
|
||||
muteLangsDescription: Séparer avec des espaces ou des retours à la ligne pour une
|
||||
condition OU (OR).
|
||||
|
@ -1265,7 +1260,7 @@ _tutorial:
|
|||
step2_2: "En fournissant quelques informations sur qui vous êtes, il sera plus facile
|
||||
pour les autres de savoir s'ils veulent voir vos publcations ou s'abonner à vous."
|
||||
step3_1: "Maintenant il est temps de vous abonner à des gens !"
|
||||
step3_2: "Vos fils d'actualités Principal et Social sont basés sur les personnes
|
||||
step3_2: "Vos fils d'actualité Principal et Social sont basés sur les personnes
|
||||
que vous êtes abonné, alors essayez de vous abonner à quelques comptes pour commencer.\n
|
||||
Cliquez sur le cercle « plus » en haut à droite d'un profil pour vous abonner."
|
||||
step4_1: "On y va."
|
||||
|
@ -1841,6 +1836,7 @@ _notification:
|
|||
reacted: a réagit à votre publication
|
||||
renoted: a boosté votre publication
|
||||
voted: a voté pour votre sondage
|
||||
andCountUsers: et {count} utilisateur(s) de plus {acted}
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Toujours afficher la colonne principale"
|
||||
columnAlign: "Aligner les colonnes"
|
||||
|
@ -2315,3 +2311,30 @@ noAltTextWarning: Certains fichiers joints n'ont aucune description. Avez-vous o
|
|||
de l'écrire ?
|
||||
showNoAltTextWarning: Afficher un avertissement si vous essayez de publier des fichiers
|
||||
sans description
|
||||
autocorrectNoteLanguage: Afficher un avertissement si la langue de publication ne
|
||||
correspond pas au résultat autodétecté
|
||||
incorrectLanguageWarning: "Il semble que votre publication est en {detected}, mais
|
||||
vous avez sélectionné {current}.\nVoulez-vous sélectionner {detected} à la place ?"
|
||||
markLocalFilesNsfwByDefault: Marquer tous les nouveaux fichiers locaux comme sensibles
|
||||
par défaut
|
||||
markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les utilisateurs
|
||||
peuvent supprimer le drapeau « sensible » (NSFW) eux-mêmes. Les fichiers existants
|
||||
ne sont pas affectés.
|
||||
noteEditHistory: Historique des publications
|
||||
media: Multimédia
|
||||
antennaLimit: Le nombre maximal d'antennes que chaque utilisateur peut créer
|
||||
showAddFileDescriptionAtFirstPost: Ouvrez automatiquement un formulaire pour écrire
|
||||
une description lorsque vous tentez de publier des fichiers sans description
|
||||
foldNotification: Grouper les notifications similaires
|
||||
cannotEditVisibility: Vous ne pouvez pas modifier la visibilité
|
||||
useThisAccountConfirm: Voulez-vous continuer avec ce compte ?
|
||||
inputAccountId: Veuillez saisir votre compte (par exemple, @firefish@info.firefish.dev)
|
||||
remoteFollow: Abonnement à distance
|
||||
copyRemoteFollowUrl: Copier l'URL d'abonnement à distance
|
||||
slashQuote: Citation enchaînée
|
||||
i18nServerInfo: Les nouveaux clients seront en {language} par défaut.
|
||||
i18nServerChange: Utilisez {language} à la place.
|
||||
i18nServerSet: Utilisez {language} pour les nouveaux clients.
|
||||
mergeThreadInTimeline: Fusionner plusieurs publications dans le même fil dans les
|
||||
fils d'actualité
|
||||
mergeRenotesInTimeline: Regrouper plusieurs boosts du même publication
|
||||
|
|
|
@ -901,7 +901,6 @@ auto: "Otomatis"
|
|||
themeColor: "Warna Jam Server"
|
||||
size: "Ukuran"
|
||||
numberOfColumn: "Jumlah per kolom"
|
||||
searchByGoogle: "Penelusuran"
|
||||
instanceDefaultLightTheme: "Tema terang bawaan ranah server"
|
||||
instanceDefaultDarkTheme: "Tema gelap bawaan ranah server"
|
||||
instanceDefaultThemeDescription: "Masukkan kode tema di format obyek."
|
||||
|
@ -986,12 +985,6 @@ _aboutFirefish:
|
|||
source: "Sumber kode"
|
||||
translation: "Terjemahkan Firefish"
|
||||
donate: "Donasi ke Firefish"
|
||||
morePatrons: "Kami sangat mengapresiasi dukungan dari banyak penolong lain yang
|
||||
tidak tercantum disini. Terima kasih! 🥰"
|
||||
patrons: "Pendukung"
|
||||
patronsList: Diurutkan secara kronologis, bukan berdasarkan jumlah donasi. Berdonasilah
|
||||
dengan tautan di atas supaya nama kamu ada di sini!
|
||||
sponsors: Sponsor Firefish
|
||||
donateTitle: Suka Firefish?
|
||||
pleaseDonateToFirefish: Silakan pertimbangkan berdonasi ke Firefish untuk mendukung
|
||||
pengembangannya.
|
||||
|
@ -1832,6 +1825,7 @@ _notification:
|
|||
reacted: mereaksi postinganmu
|
||||
renoted: memposting ulang postinganmu
|
||||
voted: memilih di angketmu
|
||||
andCountUsers: dan {count} lebih banyak pengguna {acted}
|
||||
_deck:
|
||||
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
|
||||
columnAlign: "Luruskan kolom"
|
||||
|
@ -2269,3 +2263,19 @@ incorrectLanguageWarning: "Sepertinya kirimanmu dalam bahasa {detected}, tetapi
|
|||
memilih {current}.\nApakah kamu ingin ubah bahasanya ke bahasa {detected} saja?"
|
||||
autocorrectNoteLanguage: Tampilkan peringatan jika bahasa kiriman tidak cocok dengan
|
||||
hasil yang dideteksi secara otomatis
|
||||
markLocalFilesNsfwByDefault: Tandai semua berkas lokal baru sensitif secara bawaan
|
||||
markLocalFilesNsfwByDefaultDescription: Terlepas dari pengaturan ini, pengguna dapat
|
||||
menghapus sendiri tanda NSFW. Berkas yang ada tidak berpengaruh.
|
||||
noteEditHistory: Riwayat penyuntingan kiriman
|
||||
media: Media
|
||||
antennaLimit: Jumlah antena maksimum yang dapat dibuat oleh setiap pengguna
|
||||
showAddFileDescriptionAtFirstPost: Buka formulir secara otomatis untuk menulis deskripsi
|
||||
ketika mencoba mengirim berkas tanpa deskripsi
|
||||
remoteFollow: Ikuti jarak jauh
|
||||
foldNotification: Kelompokkan notifikasi yang sama
|
||||
getQrCode: Tampilkan kode QR
|
||||
cannotEditVisibility: Kamu tidak bisa menyunting keterlihatan
|
||||
useThisAccountConfirm: Apakah kamu ingin melanjutkan dengan akun ini?
|
||||
inputAccountId: Silakan memasukkan akunmu (misalnya, @firefish@info.firefish.dev)
|
||||
copyRemoteFollowUrl: Salin URL ikuti jarak jauh
|
||||
slashQuote: Kutipan rantai
|
||||
|
|
|
@ -859,7 +859,6 @@ useDrawerReactionPickerForMobile: "Mostra sul drawer da dispositivo mobile"
|
|||
welcomeBackWithName: "Ciao {name}"
|
||||
clickToFinishEmailVerification: "Fai click su [{ok}] per completare la verifica dell'indirizzo
|
||||
email."
|
||||
searchByGoogle: "Cerca"
|
||||
indefinitely: "Senza scadenza"
|
||||
tenMinutes: "10 minuti"
|
||||
oneHour: "1 ora"
|
||||
|
@ -935,18 +934,12 @@ _aboutFirefish:
|
|||
source: "Codice sorgente"
|
||||
translation: "Traduzione di Firefish"
|
||||
donate: "Sostieni Firefish"
|
||||
morePatrons: "Apprezziamo sinceramente l'aiuto di tante altre persone non elencate
|
||||
qui. Grazie mille! 🥰"
|
||||
patrons: "Sostenitori"
|
||||
sponsors: Gli sponsor di Firefish
|
||||
misskeyContributors: Contributori di Misskey
|
||||
donateTitle: Ti piace Firefish?
|
||||
pleaseDonateToFirefish: Con una donazione puoi supportare lo sviluppo di Firefish.
|
||||
pleaseDonateToHost: Considera anche una donazione al server che ti ospita, {host},
|
||||
per contribuire ai costi che sostiene.
|
||||
donateHost: Dona a {host}
|
||||
patronsList: Elencati in ordine cronologico, non per importo. Dona con il link sopra
|
||||
per apparire in questa lista!
|
||||
_nsfw:
|
||||
respect: "Nascondi i media sensibli (NSFW)"
|
||||
ignore: "Mostra i media sensibili (NSFW)"
|
||||
|
|
|
@ -638,6 +638,7 @@ display: "表示"
|
|||
copy: "コピー"
|
||||
metrics: "メトリクス"
|
||||
overview: "概要"
|
||||
media: "メディア"
|
||||
logs: "ログ"
|
||||
delayed: "遅延"
|
||||
database: "データベース"
|
||||
|
@ -684,6 +685,9 @@ unclip: "クリップ解除"
|
|||
confirmToUnclipAlreadyClippedNote: "この投稿はすでにクリップ「{name}」に含まれています。投稿をこのクリップから除外しますか?"
|
||||
public: "公開"
|
||||
i18nInfo: "Firefishは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
||||
i18nServerInfo: "新しい端末では{language}が既定の言語になります。"
|
||||
i18nServerChange: "{language}に変更する。"
|
||||
i18nServerSet: "新しい端末での表示言語を{language}にします。"
|
||||
manageAccessTokens: "アクセストークンの管理"
|
||||
accountInfo: "アカウント情報"
|
||||
notesCount: "投稿の数"
|
||||
|
@ -885,7 +889,6 @@ socialTimeline: "ソーシャルタイムライン"
|
|||
themeColor: "テーマカラー"
|
||||
size: "サイズ"
|
||||
numberOfColumn: "列の数"
|
||||
searchByGoogle: "検索"
|
||||
instanceDefaultLightTheme: "サーバーの標準ライトテーマ"
|
||||
instanceDefaultDarkTheme: "サーバーの標準ダークテーマ"
|
||||
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
|
||||
|
@ -1011,11 +1014,11 @@ searchWordsDescription: "投稿を検索するには、ここに検索語句を
|
|||
「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n空白を含む文字列をAND検索ではなくそのまま検索したい場合、\"明日 買うもの\"\
|
||||
\ のように二重引用符 (\") で囲む必要があります。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com)
|
||||
や投稿のURLを入力し「照会」を押してください。「検索」を押すとそのIDやURLが文字通り含まれる投稿を検索します。"
|
||||
searchUsers: "投稿元(オプション)"
|
||||
searchUsers: "投稿元(省略可)"
|
||||
searchUsersDescription: "投稿検索で投稿者を絞りたい場合、@user@example.com(ローカルユーザーなら @user)の形式で投稿者のIDを入力してください。ユーザーIDではなくドメイン名
|
||||
(example.com) を指定すると、そのサーバーの投稿を検索します。\n\nme とだけ入力すると、自分の投稿を検索します。この検索結果には未収載・フォロワー限定・ダイレクト・秘密を含む全ての投稿が含まれます。\n
|
||||
\nlocal とだけ入力すると、ローカルサーバーの投稿を検索します。"
|
||||
searchRange: "投稿期間(オプション)"
|
||||
searchRange: "投稿期間(省略可)"
|
||||
searchRangeDescription: "投稿検索で投稿期間を絞りたい場合、20220615-20231031 のような形式で投稿期間を入力してください。今年の日付を指定する場合には年の指定を省略できます(0105-0106
|
||||
や 20231105-0110 のように)。\n\n開始日と終了日のどちらか一方は省略可能です。例えば -0102 とすると今年1月2日までの投稿のみを、20231026-
|
||||
とすると2023年10月26日以降の投稿のみを検索します。"
|
||||
|
@ -1109,14 +1112,10 @@ _aboutFirefish:
|
|||
source: "ソースコード"
|
||||
translation: "Firefishを翻訳"
|
||||
donate: "Firefishに寄付"
|
||||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰"
|
||||
patrons: "支援者"
|
||||
patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう!
|
||||
pleaseDonateToFirefish: Firefish開発への寄付をご検討ください。
|
||||
pleaseDonateToHost: また、このサーバー {host} の運営者への寄付もご検討ください。
|
||||
donateHost: '{host} に寄付する'
|
||||
donateTitle: Firefishを気に入りましたか?
|
||||
sponsors: Firefish の支援者
|
||||
_nsfw:
|
||||
respect: "閲覧注意のメディアは隠す"
|
||||
ignore: "閲覧注意のメディアを隠さない"
|
||||
|
@ -1906,6 +1905,7 @@ _notification:
|
|||
reacted: がリアクションしました
|
||||
renoted: がブーストしました
|
||||
voted: が投票しました
|
||||
andCountUsers: と{count}人{acted}
|
||||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
|
@ -2058,3 +2058,18 @@ ipFirstAcknowledged: IPアドレスが最初に取得された日
|
|||
driveCapacityOverride: ドライブ容量の変更
|
||||
autocorrectNoteLanguage: 設定した投稿言語が自動検出されたものと異なる場合に警告する
|
||||
incorrectLanguageWarning: "この投稿は{detected}で書かれていると判定されました。\n投稿言語を{current}ではなく{detected}にしますか?"
|
||||
markLocalFilesNsfwByDefault: このサーバーの全てのファイルをデフォルトでNSFWに設定する
|
||||
markLocalFilesNsfwByDefaultDescription: この設定が有効でも、ユーザーは自分でNSFWのフラグを外すことができます。また、この設定は既存のファイルには影響しません。
|
||||
noteEditHistory: 編集履歴
|
||||
showAddFileDescriptionAtFirstPost: 説明の無い添付ファイルを投稿しようとした際に説明を書く画面を自動で開く
|
||||
antennaLimit: 各ユーザーが作れるアンテナの最大数
|
||||
inputAccountId: 'あなたのアカウントを入力してください(例: @firefish@info.firefish.dev)'
|
||||
remoteFollow: リモートフォロー
|
||||
cannotEditVisibility: 公開範囲は変更できません
|
||||
useThisAccountConfirm: このアカウントで操作を続けますか?
|
||||
getQrCode: QRコードを表示
|
||||
copyRemoteFollowUrl: リモートからフォローするURLをコピー
|
||||
foldNotification: 同じ種類の通知をまとめて表示する
|
||||
slashQuote: 繋げて引用
|
||||
mergeRenotesInTimeline: タイムラインで同じ投稿のブーストをまとめる
|
||||
mergeThreadInTimeline: タイムラインで同じスレッドの投稿をまとめる
|
||||
|
|
|
@ -803,7 +803,6 @@ unresolved: "まだ解決してないで"
|
|||
breakFollow: "フォロワーを解除するで"
|
||||
itsOn: "オンになっとるよ"
|
||||
hide: "隠す"
|
||||
searchByGoogle: "探す"
|
||||
indefinitely: "無期限"
|
||||
file: "ファイル"
|
||||
requireAdminForView: "これを見るには管理者アカウントでログインしとらなあかんで。"
|
||||
|
@ -875,8 +874,6 @@ _aboutFirefish:
|
|||
source: "ソースコード"
|
||||
translation: "Firefishを翻訳"
|
||||
donate: "Firefishに寄付"
|
||||
morePatrons: "他にもぎょうさんの人からサポートしてもろてんねん。ほんまおおきに🥰"
|
||||
patrons: "支援者"
|
||||
misskeyContributors: フォーク元のMisskeyを作らはった人ら
|
||||
_mfm:
|
||||
cheatSheet: "MFMチートシート"
|
||||
|
|
|
@ -54,7 +54,6 @@ accountInfo: "Talɣut n umiḍan"
|
|||
emailNotification: "Ilɣa imayl"
|
||||
selectAccount: "Fren amiḍan"
|
||||
accounts: "Imiḍan"
|
||||
searchByGoogle: "Nadi"
|
||||
file: "Ifuyla"
|
||||
account: "Imiḍan"
|
||||
_email:
|
||||
|
|
|
@ -59,7 +59,6 @@ remove: "ಅಳಿಸು"
|
|||
smtpUser: "ಬಳಕೆಹೆಸರು"
|
||||
smtpPass: "ಗುಪ್ತಪದ"
|
||||
user: "ಬಳಕೆದಾರ"
|
||||
searchByGoogle: "ಹುಡುಕು"
|
||||
file: "ಕಡತಗಳು"
|
||||
_email:
|
||||
_follow:
|
||||
|
|
|
@ -851,7 +851,6 @@ auto: "자동"
|
|||
themeColor: "테마 컬러"
|
||||
size: "크기"
|
||||
numberOfColumn: "한 줄에 보일 리액션의 수"
|
||||
searchByGoogle: "검색"
|
||||
instanceDefaultLightTheme: "서버 기본 라이트 테마"
|
||||
instanceDefaultDarkTheme: "서버 기본 다크 테마"
|
||||
instanceDefaultThemeDescription: "객체 형식의 테마 코드를 입력해 주세요."
|
||||
|
@ -993,10 +992,6 @@ _aboutFirefish:
|
|||
source: "소스 코드"
|
||||
translation: "Firefish를 번역하기"
|
||||
donate: "Firefish에 기부하기"
|
||||
morePatrons: "이 외에도 다른 많은 분들이 도움을 주시고 계십니다. 감사합니다🥰"
|
||||
patrons: "후원자"
|
||||
patronsList: 기부 금액이 아닌 시간 순서로 정렬합니다. 위 링크를 통해 후원하여 당신의 이름을 새겨 보세요!
|
||||
sponsors: Firefish 스폰서
|
||||
pleaseDonateToHost: 또한, 이 서버 {host} 의 운영자에게 기부하는 것도 검토하여 주십시오.
|
||||
pleaseDonateToFirefish: Firefish의 개발에 후원하는 것을 검토하여 주십시오.
|
||||
donateHost: '{host} 에게 기부하기'
|
||||
|
|
|
@ -326,7 +326,6 @@ user: "Gebruikers"
|
|||
muteThread: "Discussies dempen "
|
||||
unmuteThread: "Dempen van discussie ongedaan maken"
|
||||
hide: "Verbergen"
|
||||
searchByGoogle: "Zoeken"
|
||||
cropImage: "Afbeelding bijsnijden"
|
||||
cropImageAsk: "Bijsnijdengevraagd"
|
||||
file: "Bestanden"
|
||||
|
|
|
@ -902,7 +902,6 @@ swipeOnDesktop: Tillat mobil-lignende sveiping på skrivebords-PC
|
|||
migration: Migrering
|
||||
useDrawerReactionPickerForMobile: Vis reaksjosnvelger som en skuff på mobil
|
||||
numberOfColumn: Antall kolonner
|
||||
searchByGoogle: Søk
|
||||
oneWeek: En uke
|
||||
file: Fil
|
||||
recentNHours: Siste {n} timer
|
||||
|
@ -988,8 +987,6 @@ _aboutFirefish:
|
|||
pleaseDonateToFirefish: Du kan vurdere å donere en slant til Firefish for å støtte
|
||||
videre utvikling og feilretting.
|
||||
donateHost: Donér til {host}
|
||||
morePatrons: Vi er også takknemlige for bidragene fra mange andre som ikke er listet
|
||||
her. Takk til dere alle! 🥰
|
||||
contributors: Hovedutviklere
|
||||
source: Kildekode
|
||||
allContributors: Alle bidragsytere
|
||||
|
@ -997,10 +994,6 @@ _aboutFirefish:
|
|||
pleaseDonateToHost: Du kan også vurdere å donere til hjemme-tjeneren din, {host},
|
||||
for å hjelpe dem med driftskostnadene for tjenesten.
|
||||
about: Firefish ble opprettet av ThatOneCalculator i 2022, basert på Misskey.
|
||||
sponsors: Firefishs sponsorer
|
||||
patrons: Firefishs patroner
|
||||
patronsList: Listen er kronologisk, ikke etter donert beløp. Doner med lenken over
|
||||
for å få navnet ditt her!
|
||||
isBot: Denne kontoen er en bot
|
||||
_nsfw:
|
||||
respect: Skjul NSFW-merket media
|
||||
|
|
|
@ -867,7 +867,6 @@ tablet: "Tablet"
|
|||
auto: "Automatycznie"
|
||||
size: "Rozmiar"
|
||||
numberOfColumn: "Liczba kolumn"
|
||||
searchByGoogle: "Szukaj"
|
||||
indefinitely: "Dożywotnio"
|
||||
file: "Pliki"
|
||||
logoutConfirm: "Czy na pewno chcesz się wylogować?"
|
||||
|
@ -991,9 +990,6 @@ _aboutFirefish:
|
|||
source: "Kod źródłowy"
|
||||
translation: "Tłumacz Firefish"
|
||||
donate: "Przekaż darowiznę na Firefish"
|
||||
morePatrons: "Naprawdę doceniam wsparcie ze strony wielu niewymienionych tu osób.
|
||||
Dziękuję! 🥰"
|
||||
patrons: "Wspierający"
|
||||
_nsfw:
|
||||
respect: "Ukrywaj media NSFW"
|
||||
ignore: "Nie ukrywaj mediów NSFW"
|
||||
|
|
|
@ -497,7 +497,6 @@ smtpPass: "Senha"
|
|||
clearCache: "Limpar memória transitória"
|
||||
info: "Informações"
|
||||
user: "Usuários"
|
||||
searchByGoogle: "Buscar"
|
||||
file: "Ficheiros"
|
||||
_email:
|
||||
_follow:
|
||||
|
|
|
@ -692,7 +692,6 @@ user: "Utilizatori"
|
|||
administration: "Gestionare"
|
||||
middle: "Mediu"
|
||||
sent: "Trimite"
|
||||
searchByGoogle: "Caută"
|
||||
file: "Fișiere"
|
||||
_email:
|
||||
_follow:
|
||||
|
|
|
@ -894,7 +894,6 @@ auto: "Автоматически"
|
|||
themeColor: "Цвет темы сервера"
|
||||
size: "Размер"
|
||||
numberOfColumn: "Количество столбцов"
|
||||
searchByGoogle: "Поиск"
|
||||
instanceDefaultLightTheme: "Светлая тема по умолчанию для всего сервера"
|
||||
instanceDefaultDarkTheme: "Темная тема по умолчанию для всего сервера"
|
||||
indefinitely: "вечно"
|
||||
|
@ -987,12 +986,6 @@ _aboutFirefish:
|
|||
source: "Исходный код"
|
||||
translation: "Перевод Firefish"
|
||||
donate: "Пожертвование на Firefish"
|
||||
morePatrons: "Большое спасибо и многим другим, кто принял участие в этом проекте!
|
||||
🥰"
|
||||
patrons: "Материальная поддержка"
|
||||
patronsList: Перечислены в хронологическом порядке, а не по размеру пожертвования.
|
||||
Сделайте взнос по ссылке выше, чтобы ваше имя было здесь!
|
||||
sponsors: Спонсоры Firefish
|
||||
donateTitle: Понравился Firefish?
|
||||
pleaseDonateToFirefish: Пожалуйста, поддержите разработку Firefish.
|
||||
pleaseDonateToHost: Также не забудьте поддержать ваш домашний сервер {host}, чтобы
|
||||
|
|
|
@ -886,7 +886,6 @@ auto: "Automaticky"
|
|||
themeColor: "Farba témy"
|
||||
size: "Veľkosť"
|
||||
numberOfColumn: "Počet stĺpcov"
|
||||
searchByGoogle: "Hľadať cez Google"
|
||||
instanceDefaultLightTheme: "Predvolená svetlá téma"
|
||||
instanceDefaultDarkTheme: "Predvolená tmavá téma"
|
||||
instanceDefaultThemeDescription: "Vložte kód témy v objektovom formáte"
|
||||
|
@ -1037,9 +1036,6 @@ _aboutFirefish:
|
|||
source: "Zdrojový kód"
|
||||
translation: "Preložiť Firefish"
|
||||
donate: "Podporiť Firefish"
|
||||
morePatrons: "Takisto oceňujeme podporu mnoých ďalších, ktorí tu nie sú uvedení.
|
||||
Ďakujeme! 🥰"
|
||||
patrons: "Prispievatelia"
|
||||
_nsfw:
|
||||
respect: "Skryť NSFW médiá"
|
||||
ignore: "Neskrývať NSFW médiá"
|
||||
|
|
|
@ -268,7 +268,6 @@ smtpUser: "Användarnamn"
|
|||
smtpPass: "Lösenord"
|
||||
clearCache: "Rensa cache"
|
||||
user: "Användare"
|
||||
searchByGoogle: "Sök"
|
||||
file: "Filer"
|
||||
_email:
|
||||
_follow:
|
||||
|
|
|
@ -873,7 +873,6 @@ auto: "อัตโนมัติ"
|
|||
themeColor: "สีข้อความเลื่อนของเซิร์ฟเวอร์"
|
||||
size: "ขนาด"
|
||||
numberOfColumn: "จำนวนคอลัมน์"
|
||||
searchByGoogle: "ค้นหา"
|
||||
instanceDefaultLightTheme: "ธีมสว่างค่าเริ่มต้นของเซิร์ฟเวอร์"
|
||||
instanceDefaultDarkTheme: "ธีมมืดค่าเริ่มต้นของเซิร์ฟเวอร์"
|
||||
instanceDefaultThemeDescription: "ป้อนรหัสธีมในรูปแบบออบเจ็กต์"
|
||||
|
@ -1023,9 +1022,6 @@ _aboutFirefish:
|
|||
source: "ซอร์สโค้ด"
|
||||
translation: "รับแปลภาษา Firefish"
|
||||
donate: "บริจาคให้กับ Firefish"
|
||||
morePatrons: "เราขอขอบคุณสำหรับความช่วยเหลือจากผู้ช่วยอื่นๆ ที่ไม่ได้ระบุไว้ที่นี่นะ
|
||||
ขอขอบคุณ! 🥰"
|
||||
patrons: "สมาชิกพันธมิตร"
|
||||
_nsfw:
|
||||
respect: "ซ่อนสื่อ NSFW"
|
||||
ignore: "อย่าซ่อนสื่อ NSFW"
|
||||
|
|
|
@ -48,7 +48,6 @@ remove: "Sil"
|
|||
smtpUser: "Kullanıcı Adı"
|
||||
smtpPass: "Şifre"
|
||||
user: "Kullanıcı"
|
||||
searchByGoogle: "Arama"
|
||||
_mfm:
|
||||
search: "Arama"
|
||||
play: MFM'i çal
|
||||
|
@ -1911,14 +1910,9 @@ _preferencesBackups:
|
|||
updatedAt: 'Güncelleme tarihi: {date} {time}'
|
||||
cannotLoad: Yüklenemedi
|
||||
_aboutFirefish:
|
||||
patronsList: Bağış büyüklüğüne göre değil, kronolojik olarak listelenmiştir. Adınızı
|
||||
buraya almak için yukarıdaki bağlantıyla bağış yapın!
|
||||
about: Firefish, 2022'den beri geliştirilmekte olan ThatOneCalculator tarafından
|
||||
yapılan bir Misskey çatalıdır.
|
||||
allContributors: Tüm katkıda bulunanlar
|
||||
patrons: Firefish patronları
|
||||
morePatrons: Burada listelenmeyen diğer birçok yardımcının desteğini de takdir ediyoruz.
|
||||
Teşekkür ederim! 🥰
|
||||
donate: Firefish'e bağışta bulunun
|
||||
contributors: Ana katkıda bulunanlar
|
||||
source: Kaynak Kodu
|
||||
|
@ -1929,7 +1923,6 @@ _aboutFirefish:
|
|||
pleaseDonateToHost: İşletme maliyetlerini desteklemek için lütfen ev sunucunuz {host}'a
|
||||
bağış yapmayı da düşünün.
|
||||
donateHost: '{ev sahibi} için bağış yapın'
|
||||
sponsors: Firefish sponsorları
|
||||
misskeyContributors: Misskey'e katkıda bulunanlar
|
||||
_weekday:
|
||||
saturday: Cumartesi
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
_lang_: "ياپونچە"
|
||||
search: "ئىزدەش"
|
||||
searchByGoogle: "ئىزدەش"
|
||||
_mfm:
|
||||
search: "ئىزدەش"
|
||||
|
|
|
@ -788,7 +788,6 @@ global: "Глобальна"
|
|||
sent: "Відправлене"
|
||||
hashtags: "Хештеґ"
|
||||
hide: "Сховати"
|
||||
searchByGoogle: "Пошук"
|
||||
indefinitely: "Ніколи"
|
||||
file: "Файли"
|
||||
reverse: "Переворот"
|
||||
|
@ -826,17 +825,11 @@ _aboutFirefish:
|
|||
source: "Вихідний код"
|
||||
translation: "Перекладати Firefish"
|
||||
donate: "Пожертвувати Firefish"
|
||||
morePatrons: "Ми дуже цінуємо підтримку багатьох інших помічників, не перелічених
|
||||
тут. Дякуємо! 🥰"
|
||||
patrons: "Підтримали"
|
||||
patronsList: Перераховані в хронологічному порядку, а не за розміром пожертви. Зробіть
|
||||
внесок за посиланням вище, щоб ваше ім'я було тут!
|
||||
donateTitle: Сподобався Firefish?
|
||||
pleaseDonateToFirefish: Будь ласка, підтримайте розробку Firefish.
|
||||
pleaseDonateToHost: Також не забудьте підтримати ваш домашній сервер {host}, щоб
|
||||
допомогти з його операційними витратами.
|
||||
donateHost: Зробити внесок на рахунок {host}
|
||||
sponsors: Спонсори Firefish
|
||||
misskeyContributors: Контрибутори Misskey
|
||||
_nsfw:
|
||||
respect: "Приховувати NSFW медіа"
|
||||
|
|
|
@ -892,7 +892,6 @@ auto: "Tự động"
|
|||
themeColor: "Màu theme"
|
||||
size: "Kích thước"
|
||||
numberOfColumn: "Số lượng cột"
|
||||
searchByGoogle: "Google"
|
||||
instanceDefaultLightTheme: "Theme máy chủ Sáng-Rộng"
|
||||
instanceDefaultDarkTheme: "Theme máy chủ Tối-Rộng"
|
||||
instanceDefaultThemeDescription: "Nhập mã theme trong định dạng đối tượng."
|
||||
|
@ -1051,15 +1050,10 @@ _aboutFirefish:
|
|||
source: "Mã nguồn"
|
||||
translation: "Dịch Firefish"
|
||||
donate: "Ủng hộ Firefish"
|
||||
morePatrons: "Chúng tôi cũng trân trọng sự hỗ trợ của nhiều người đóng góp khác
|
||||
không được liệt kê ở đây. Cảm ơn! 🥰"
|
||||
patrons: "Người ủng hộ"
|
||||
patronsList: Liệt kê theo thứ tự, không theo số tiền ủng hộ. Hãy để tên bạn ở đây!
|
||||
donateTitle: Thích Firefish?
|
||||
pleaseDonateToFirefish: Hãy cân nhắc ủng hộ Firefish phát triển.
|
||||
donateHost: Ủng hộ {host}
|
||||
pleaseDonateToHost: Cũng như ủng hộ chi phí vận hành máy chủ {host} của bạn.
|
||||
sponsors: Nhà tài trợ Firefish
|
||||
misskeyContributors: Người đóng góp Misskey
|
||||
_nsfw:
|
||||
respect: "Ẩn nội dung NSFW"
|
||||
|
|
|
@ -269,7 +269,7 @@ agreeTo: "我同意 {0}"
|
|||
tos: "服务条款"
|
||||
start: "开始"
|
||||
home: "首页"
|
||||
remoteUserCaution: "由于此用户来自其它服务器,显示的信息不完整。"
|
||||
remoteUserCaution: "此用户来自其它服务器,显示信息会不完整。"
|
||||
activity: "活动"
|
||||
images: "图片"
|
||||
birthday: "生日"
|
||||
|
@ -340,6 +340,7 @@ invite: "邀请"
|
|||
driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
|
||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||
inMb: "以兆字节 (MegaByte) 为单位"
|
||||
antennaLimit: "每个用户最多可以创建的天线数量"
|
||||
iconUrl: "图标 URL"
|
||||
bannerUrl: "横幅图 URL"
|
||||
backgroundImageUrl: "背景图 URL"
|
||||
|
@ -563,6 +564,7 @@ deletedNote: "已删除的帖子"
|
|||
invisibleNote: "隐藏的帖子"
|
||||
enableInfiniteScroll: "滚动页面以载入更多内容"
|
||||
visibility: "可见性"
|
||||
cannotEditVisibility: "不能编辑帖子的可见性"
|
||||
poll: "调查问卷"
|
||||
useCw: "隐藏内容"
|
||||
enablePlayer: "打开播放器"
|
||||
|
@ -665,6 +667,9 @@ unclip: "移除便签"
|
|||
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?"
|
||||
public: "公开"
|
||||
i18nInfo: "Firefish 已经被志愿者们翻译成了各种语言。如果您也有兴趣,可以通过 {link} 帮助翻译。"
|
||||
i18nServerInfo: "新客户端将默认使用 {language}。"
|
||||
i18nServerChange: "改为 {language}。"
|
||||
i18nServerSet: "设定新客户端使用 {language}。"
|
||||
manageAccessTokens: "管理访问令牌"
|
||||
accountInfo: "账号信息"
|
||||
notesCount: "帖子数量"
|
||||
|
@ -851,7 +856,6 @@ auto: "自动"
|
|||
themeColor: "服务器滚动条颜色"
|
||||
size: "大小"
|
||||
numberOfColumn: "列数"
|
||||
searchByGoogle: "搜索"
|
||||
instanceDefaultLightTheme: "服务器默认浅色主题"
|
||||
instanceDefaultDarkTheme: "服务器默认深色主题"
|
||||
instanceDefaultThemeDescription: "以对象格式键入主题代码。"
|
||||
|
@ -878,6 +882,8 @@ driveCapOverrideCaption: "输入 0 或以下的值将容量重置为默认值。
|
|||
requireAdminForView: "您需要使用管理员账号登录才能查看。"
|
||||
isSystemAccount: "该账号由系统自动创建。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。"
|
||||
typeToConfirm: "输入 {x} 以确认操作"
|
||||
useThisAccountConfirm: "您想使用此帐户继续执行此操作吗?"
|
||||
inputAccountId: "请输入您的帐户(例如 @firefish@info.firefish.dev )"
|
||||
deleteAccount: "删除账号"
|
||||
document: "文档"
|
||||
numberOfPageCache: "缓存页数"
|
||||
|
@ -996,10 +1002,6 @@ _aboutFirefish:
|
|||
source: "源代码"
|
||||
translation: "翻译 Firefish"
|
||||
donate: "赞助 Firefish"
|
||||
morePatrons: "还有很多其它的人也在支持我们,非常感谢🥰"
|
||||
patrons: "Firefish 赞助者"
|
||||
patronsList: 按时间顺序而不是捐赠金额排列。通过上面的链接捐款,让您的名字出现在这里!
|
||||
sponsors: Firefish 赞助者们
|
||||
donateTitle: 喜欢 Firefish 吗?
|
||||
pleaseDonateToFirefish: 请考虑赞助 Firefish 以支持其开发。
|
||||
pleaseDonateToHost: 也请考虑赞助您的主服务器 {host},以帮助支持其运营成本。
|
||||
|
@ -1390,7 +1392,7 @@ _poll:
|
|||
_visibility:
|
||||
public: "公开"
|
||||
publicDescription: "您的帖子将出现在公共时间线上"
|
||||
home: "不公开"
|
||||
home: "悄悄公开"
|
||||
homeDescription: "仅发送至首页时间线"
|
||||
followers: "仅关注者"
|
||||
followersDescription: "仅对您的关注者和提及的用户可见"
|
||||
|
@ -1791,6 +1793,7 @@ _notification:
|
|||
reacted: 回应了您的帖子
|
||||
voted: 在您的问卷调查中投了票
|
||||
renoted: 转发了您的帖子
|
||||
andCountUsers: "和其他 {count} 名用户{acted}"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "总是显示主列"
|
||||
columnAlign: "列对齐"
|
||||
|
@ -1976,6 +1979,9 @@ origin: 起源
|
|||
confirm: 确认
|
||||
importZip: 导入 ZIP
|
||||
exportZip: 导出 ZIP
|
||||
getQrCode: "获取二维码"
|
||||
remoteFollow: "远程关注"
|
||||
copyRemoteFollowUrl: "复制远程关注 URL"
|
||||
emojiPackCreator: 表情包创建工具
|
||||
objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
|
||||
的端点 URL。
|
||||
|
@ -2058,5 +2064,12 @@ searchRangeDescription: "如果您要过滤时间段,请按以下格式输入
|
|||
messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。"
|
||||
noAltTextWarning: 有些附件没有描述。您是否忘记写描述了?
|
||||
showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告
|
||||
showAddFileDescriptionAtFirstPost: 当您首次尝试发布没有描述的帖子附件时自动弹出添加描述页面
|
||||
autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候显示警告
|
||||
incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?"
|
||||
noteEditHistory: "帖子编辑历史"
|
||||
media: 媒体
|
||||
slashQuote: "斜杠引用"
|
||||
foldNotification: "将通知按同类型分组"
|
||||
mergeThreadInTimeline: "将时间线内的连续回复合并成一串"
|
||||
mergeRenotesInTimeline: "合并同一个帖子的转发"
|
||||
|
|
|
@ -661,6 +661,9 @@ unclip: "解除摘錄"
|
|||
confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?"
|
||||
public: "公開"
|
||||
i18nInfo: "Firefish已經被志願者們翻譯成各種語言版本,如果想要幫忙的話,可以進入{link}幫助翻譯。"
|
||||
i18nServerInfo: "新客戶端將默認使用 {language}。"
|
||||
i18nServerChange: "改為 {language}。"
|
||||
i18nServerSet: "設定新客戶端使用 {language}。"
|
||||
manageAccessTokens: "管理存取權杖"
|
||||
accountInfo: "帳戶資訊"
|
||||
notesCount: "貼文數量"
|
||||
|
@ -847,7 +850,6 @@ auto: "自動"
|
|||
themeColor: "主題顏色"
|
||||
size: "大小"
|
||||
numberOfColumn: "列數"
|
||||
searchByGoogle: "搜尋"
|
||||
instanceDefaultLightTheme: "伺服器預設的淺色主題"
|
||||
instanceDefaultDarkTheme: "伺服器預設的深色主題"
|
||||
instanceDefaultThemeDescription: "輸入物件形式的主題代碼。"
|
||||
|
@ -992,10 +994,6 @@ _aboutFirefish:
|
|||
source: "原始碼"
|
||||
translation: "翻譯Firefish"
|
||||
donate: "贊助Firefish"
|
||||
morePatrons: "還有許許多多幫助我們的其他人,非常感謝你們。 🥰"
|
||||
patrons: "贊助者"
|
||||
patronsList: 按時間順序列出,而不是按贊助規模列出。使用上面的連結贊助,在這裡獲得顯示您名字的機會!
|
||||
sponsors: Firefish 贊助者們
|
||||
donateTitle: 覺得 Firefish 棒嗎?
|
||||
pleaseDonateToFirefish: 請考慮向 Firefish 贊助以支持其發展。
|
||||
pleaseDonateToHost: 還請考慮捐贈給您在使用的伺服器 {host},以支援龐大的運營成本。
|
||||
|
|
50
package.json
50
package.json
|
@ -1,52 +1,54 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240326",
|
||||
"version": "20240516",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.5",
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm run build",
|
||||
"build": "pnpm node ./scripts/build.mjs && pnpm run gulp",
|
||||
"build": "pnpm --recursive --color run build && pnpm node ./scripts/copy-index.mjs && pnpm run build:assets",
|
||||
"build:assets": "pnpm node ./scripts/copy-assets.mjs",
|
||||
"build:debug": "pnpm run clean && pnpm --recursive --color run build:debug && pnpm node ./scripts/copy-index-dev.mjs && pnpm run build:assets",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:container": "pnpm run gulp && pnpm run migrate && pnpm run start",
|
||||
"start:container": "pnpm run build:assets && pnpm run migrate && pnpm run start",
|
||||
"start:test": "pnpm --filter backend run start:test",
|
||||
"init": "pnpm run migrate",
|
||||
"migrate": "pnpm --filter backend run migration:run",
|
||||
"revertmigration": "pnpm --filter backend run migration:revert",
|
||||
"gulp": "gulp build",
|
||||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.mjs",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
"lint": "pnpm -r --parallel run lint",
|
||||
"lint": "pnpm run lint:ts; pnpm run lint:rs",
|
||||
"lint:ts": "pnpm --filter !firefish-js -r --parallel run lint",
|
||||
"lint:rs": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt --all --",
|
||||
"debug": "pnpm run build:debug && pnpm run start",
|
||||
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
"test": "pnpm run mocha",
|
||||
"format": "pnpm -r --parallel run format",
|
||||
"test": "pnpm run test:ts && pnpm run test:rs",
|
||||
"test:ts": "pnpm run mocha",
|
||||
"test:rs": "cargo test",
|
||||
"format": "pnpm run format:ts; pnpm run format:rs",
|
||||
"format:ts": "pnpm -r --parallel run format",
|
||||
"format:rs": "cargo fmt --all --",
|
||||
"clean": "pnpm node ./scripts/clean-built.mjs",
|
||||
"clean-npm": "pnpm node ./scripts/clean-npm.mjs",
|
||||
"clean-cargo": "pnpm node ./scripts/clean-cargo.mjs",
|
||||
"clean-cargo": "cargo clean",
|
||||
"clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-yaml": "4.1.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0"
|
||||
"js-yaml": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.6.2",
|
||||
"@biomejs/cli-darwin-arm64": "^1.6.2",
|
||||
"@biomejs/cli-darwin-x64": "^1.6.2",
|
||||
"@biomejs/cli-linux-arm64": "^1.6.2",
|
||||
"@biomejs/cli-linux-x64": "^1.6.2",
|
||||
"@types/node": "20.11.30",
|
||||
"execa": "8.0.1",
|
||||
"pnpm": "8.15.5",
|
||||
"typescript": "5.4.3"
|
||||
"@biomejs/biome": "1.7.3",
|
||||
"@biomejs/cli-darwin-arm64": "1.7.3",
|
||||
"@biomejs/cli-darwin-x64": "1.7.3",
|
||||
"@biomejs/cli-linux-arm64": "1.7.3",
|
||||
"@biomejs/cli-linux-x64": "1.7.3",
|
||||
"@types/node": "20.12.12",
|
||||
"execa": "9.1.0",
|
||||
"pnpm": "9.1.1",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ This directory contains all of the packages Firefish uses.
|
|||
|
||||
- `backend`: Main backend code written in TypeScript for NodeJS
|
||||
- `backend-rs`: Backend code written in Rust, bound to NodeJS by [NAPI-RS](https://napi.rs/)
|
||||
- `macro-rs`: Procedural macros for backend-rs
|
||||
- `client`: Web interface written in Vue3 and TypeScript
|
||||
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
|
||||
- `firefish-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/firefish-js) for public use
|
||||
- `firefish-js`: TypeScript SDK for both backend and client
|
||||
- `megalodon`: TypeScript library used for partial Mastodon API compatibility
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
.github
|
||||
npm
|
||||
.eslintrc
|
||||
.prettierignore
|
||||
rustfmt.toml
|
||||
yarn.lock
|
||||
*.node
|
||||
.yarn
|
||||
__test__
|
||||
renovate.json
|
|
@ -7,36 +7,52 @@ rust-version = "1.74"
|
|||
[features]
|
||||
default = []
|
||||
napi = ["dep:napi", "dep:napi-derive"]
|
||||
ci = []
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.78"
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4.35"
|
||||
cuid2 = "0.1.2"
|
||||
jsonschema = "0.17.1"
|
||||
once_cell = "1.19.0"
|
||||
parse-display = "0.8.2"
|
||||
rand = "0.8.5"
|
||||
schemars = { version = "0.8.16", features = ["chrono"] }
|
||||
sea-orm = { version = "0.12.15", features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
thiserror = "1.0.58"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
macro-rs = { workspace = true }
|
||||
|
||||
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
napi = { version = "2.16.0", default-features = false, features = ["napi9", "tokio_rt"], optional = true }
|
||||
napi-derive = { version = "2.16.0", optional = true }
|
||||
basen = "0.1.0"
|
||||
napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] }
|
||||
napi-derive = { workspace = true, optional = true }
|
||||
|
||||
argon2 = { workspace = true, features = ["std"] }
|
||||
async-trait = { workspace = true }
|
||||
basen = { workspace = true }
|
||||
bb8 = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cuid2 = { workspace = true }
|
||||
emojis = { workspace = true }
|
||||
idna = { workspace = true }
|
||||
image = { workspace = true }
|
||||
isahc = { workspace = true }
|
||||
nom-exif = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
openssl = { workspace = true, features = ["vendored"] }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true, default-features = false, features = ["streams", "tokio-comp"] }
|
||||
regex = { workspace = true }
|
||||
rmp-serde = { workspace = true }
|
||||
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
sysinfo = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
web-push = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.0"
|
||||
pretty_assertions = { workspace = true }
|
||||
tokio-test = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.1.2"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
napi-build = { workspace = true }
|
||||
|
|
|
@ -1,11 +1,35 @@
|
|||
recursive_wildcard = $(foreach d, $(wildcard $(1:=/*)), $(call recursive_wildcard, $d, $2) $(filter $(subst *, %, $2), $d))
|
||||
|
||||
SRC := Cargo.toml
|
||||
SRC += $(call recursive_wildcard, src, *)
|
||||
|
||||
|
||||
.PHONY: regenerate-entities
|
||||
regenerate-entities:
|
||||
rm --recursive --force src/model/entity
|
||||
sea-orm-cli generate entity \
|
||||
--output-dir='src/model/entity' \
|
||||
--database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)'
|
||||
--output-dir='src/model/entity' \
|
||||
--database-url='postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@localhost:25432/$(POSTGRES_DB)' \
|
||||
--date-time-crate='chrono' \
|
||||
--model-extra-attributes='NAPI_EXTRA_ATTR_PLACEHOLDER' && \
|
||||
for file in src/model/entity/*; do \
|
||||
base=$$(basename -- "$${file}"); \
|
||||
jsname=$$(printf '%s\n' "$${base%.*}" | perl -pe 's/(^|_)./uc($$&)/ge;s/_//g'); \
|
||||
attribute=$$(printf 'cfg_attr(feature = "napi", napi_derive::napi(object, js_name = "%s", use_nullable = true))' "$${jsname}"); \
|
||||
sed -i "s/NAPI_EXTRA_ATTR_PLACEHOLDER/$${attribute}/" "$${file}"; \
|
||||
sed -i 's/#\[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)\]/#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]\n#[serde(rename_all = "camelCase")]/' "$${file}"; \
|
||||
done
|
||||
sed -i 's/#\[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)\]/#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, serde::Serialize, serde::Deserialize)]\n#[serde(rename_all = "camelCase")]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi(string_enum = "camelCase"))]/' \
|
||||
src/model/entity/sea_orm_active_enums.rs
|
||||
cargo fmt --all --
|
||||
|
||||
index.js:
|
||||
.PHONY: update-index
|
||||
update-index: index.js index.d.ts
|
||||
|
||||
index.js index.d.ts: $(SRC)
|
||||
NODE_OPTIONS='--max_old_space_size=3072' pnpm run build:debug
|
||||
[ -f built/index.js ]
|
||||
rm --force index.js
|
||||
[ -f built/index.js ] && [ -f built/index.d.ts ]
|
||||
rm --force index.js index.d.ts
|
||||
cp built/index.js index.js
|
||||
cp built/index.d.ts index.d.ts
|
||||
sed -i 's/^ \*r"/ */g' index.d.ts
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import test from "ava";
|
||||
|
||||
import {
|
||||
convertId,
|
||||
IdConvertType,
|
||||
nativeInitIdGenerator,
|
||||
nativeCreateId,
|
||||
nativeRandomStr,
|
||||
} from "../built/index.js";
|
||||
|
||||
test("convert to mastodon id", (t) => {
|
||||
t.is(convertId("9gf61ehcxv", IdConvertType.MastodonId), "960365976481219");
|
||||
t.is(
|
||||
convertId("9fbr9z0wbrjqyd3u", IdConvertType.MastodonId),
|
||||
"2083785058661759970208986",
|
||||
);
|
||||
t.is(
|
||||
convertId("9fbs680oyviiqrol9md73p8g", IdConvertType.MastodonId),
|
||||
"5878598648988104013828532260828151168",
|
||||
);
|
||||
});
|
||||
|
||||
test("create cuid2 with timestamp prefix", (t) => {
|
||||
nativeInitIdGenerator(16, "");
|
||||
t.not(nativeCreateId(Date.now()), nativeCreateId(Date.now()));
|
||||
t.is(nativeCreateId(Date.now()).length, 16);
|
||||
});
|
||||
|
||||
test("create random string", (t) => {
|
||||
t.not(nativeRandomStr(16), nativeRandomStr(16));
|
||||
t.is(nativeRandomStr(24).length, 24);
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
// watch the version in the project root package.json
|
||||
println!("cargo:rerun-if-changed=../../package.json");
|
||||
|
||||
// napi
|
||||
napi_build::setup();
|
||||
}
|
||||
|
|
1327
packages/backend-rs/index.d.ts
vendored
Normal file
1327
packages/backend-rs/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -224,17 +224,32 @@ switch (platform) {
|
|||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'backend-rs.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./backend-rs.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('backend-rs-linux-arm-gnueabihf')
|
||||
if (isMusl()) {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'backend-rs.linux-arm-musleabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./backend-rs.linux-arm-musleabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('backend-rs-linux-arm-musleabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} else {
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'backend-rs.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./backend-rs.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('backend-rs-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'riscv64':
|
||||
|
@ -295,11 +310,88 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { nativeRandomStr, IdConvertType, convertId, nativeGetTimestamp, nativeCreateId, nativeInitIdGenerator } = nativeBinding
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, updateAntennasOnNewNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
|
||||
|
||||
module.exports.nativeRandomStr = nativeRandomStr
|
||||
module.exports.IdConvertType = IdConvertType
|
||||
module.exports.convertId = convertId
|
||||
module.exports.nativeGetTimestamp = nativeGetTimestamp
|
||||
module.exports.nativeCreateId = nativeCreateId
|
||||
module.exports.nativeInitIdGenerator = nativeInitIdGenerator
|
||||
module.exports.SECOND = SECOND
|
||||
module.exports.MINUTE = MINUTE
|
||||
module.exports.HOUR = HOUR
|
||||
module.exports.DAY = DAY
|
||||
module.exports.USER_ONLINE_THRESHOLD = USER_ONLINE_THRESHOLD
|
||||
module.exports.USER_ACTIVE_THRESHOLD = USER_ACTIVE_THRESHOLD
|
||||
module.exports.FILE_TYPE_BROWSERSAFE = FILE_TYPE_BROWSERSAFE
|
||||
module.exports.loadEnv = loadEnv
|
||||
module.exports.loadConfig = loadConfig
|
||||
module.exports.stringToAcct = stringToAcct
|
||||
module.exports.acctToString = acctToString
|
||||
module.exports.greet = greet
|
||||
module.exports.initializeRustLogger = initializeRustLogger
|
||||
module.exports.showServerInfo = showServerInfo
|
||||
module.exports.isBlockedServer = isBlockedServer
|
||||
module.exports.isSilencedServer = isSilencedServer
|
||||
module.exports.isAllowedServer = isAllowedServer
|
||||
module.exports.checkWordMute = checkWordMute
|
||||
module.exports.getFullApAccount = getFullApAccount
|
||||
module.exports.isSelfHost = isSelfHost
|
||||
module.exports.isSameOrigin = isSameOrigin
|
||||
module.exports.extractHost = extractHost
|
||||
module.exports.toPuny = toPuny
|
||||
module.exports.isUnicodeEmoji = isUnicodeEmoji
|
||||
module.exports.sqlLikeEscape = sqlLikeEscape
|
||||
module.exports.safeForSql = safeForSql
|
||||
module.exports.formatMilliseconds = formatMilliseconds
|
||||
module.exports.getImageSizeFromUrl = getImageSizeFromUrl
|
||||
module.exports.getNoteSummary = getNoteSummary
|
||||
module.exports.isQuote = isQuote
|
||||
module.exports.isSafeUrl = isSafeUrl
|
||||
module.exports.latestVersion = latestVersion
|
||||
module.exports.toMastodonId = toMastodonId
|
||||
module.exports.fromMastodonId = fromMastodonId
|
||||
module.exports.fetchMeta = fetchMeta
|
||||
module.exports.metaToPugArgs = metaToPugArgs
|
||||
module.exports.nyaify = nyaify
|
||||
module.exports.hashPassword = hashPassword
|
||||
module.exports.verifyPassword = verifyPassword
|
||||
module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm
|
||||
module.exports.decodeReaction = decodeReaction
|
||||
module.exports.countReactions = countReactions
|
||||
module.exports.toDbReaction = toDbReaction
|
||||
module.exports.removeOldAttestationChallenges = removeOldAttestationChallenges
|
||||
module.exports.cpuInfo = cpuInfo
|
||||
module.exports.cpuUsage = cpuUsage
|
||||
module.exports.memoryUsage = memoryUsage
|
||||
module.exports.storageUsage = storageUsage
|
||||
module.exports.AntennaSrcEnum = AntennaSrcEnum
|
||||
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
|
||||
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
|
||||
module.exports.NoteVisibilityEnum = NoteVisibilityEnum
|
||||
module.exports.NotificationTypeEnum = NotificationTypeEnum
|
||||
module.exports.PageVisibilityEnum = PageVisibilityEnum
|
||||
module.exports.PollNotevisibilityEnum = PollNotevisibilityEnum
|
||||
module.exports.RelayStatusEnum = RelayStatusEnum
|
||||
module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
|
||||
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
|
||||
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
||||
module.exports.updateAntennasOnNewNote = updateAntennasOnNewNote
|
||||
module.exports.fetchNodeinfo = fetchNodeinfo
|
||||
module.exports.nodeinfo_2_1 = nodeinfo_2_1
|
||||
module.exports.nodeinfo_2_0 = nodeinfo_2_0
|
||||
module.exports.Protocol = Protocol
|
||||
module.exports.Inbound = Inbound
|
||||
module.exports.Outbound = Outbound
|
||||
module.exports.watchNote = watchNote
|
||||
module.exports.unwatchNote = unwatchNote
|
||||
module.exports.PushNotificationKind = PushNotificationKind
|
||||
module.exports.sendPushNotification = sendPushNotification
|
||||
module.exports.publishToChannelStream = publishToChannelStream
|
||||
module.exports.ChatEvent = ChatEvent
|
||||
module.exports.publishToChatStream = publishToChatStream
|
||||
module.exports.ChatIndexEvent = ChatIndexEvent
|
||||
module.exports.publishToChatIndexStream = publishToChatIndexStream
|
||||
module.exports.publishToBroadcastStream = publishToBroadcastStream
|
||||
module.exports.publishToGroupChatStream = publishToGroupChatStream
|
||||
module.exports.publishToModerationStream = publishToModerationStream
|
||||
module.exports.getTimestamp = getTimestamp
|
||||
module.exports.genId = genId
|
||||
module.exports.genIdAt = genIdAt
|
||||
module.exports.generateSecureRandomString = generateSecureRandomString
|
||||
module.exports.generateUserToken = generateUserToken
|
||||
|
|
|
@ -22,27 +22,14 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.18.0",
|
||||
"ava": "6.1.2"
|
||||
},
|
||||
"ava": {
|
||||
"timeout": "3m"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
"@napi-rs/cli": "2.18.3"
|
||||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"build": "napi build --features napi --platform --release --cargo-flags=--locked ./built/",
|
||||
"build:debug": "napi build --features napi --platform --cargo-flags=--locked ./built/",
|
||||
"build": "napi build --features napi --no-const-enum --platform --release ./built/",
|
||||
"build:debug": "napi build --features napi --no-const-enum --platform ./built/",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"test": "pnpm run cargo:test && pnpm run build:napi && ava",
|
||||
"universal": "napi universal",
|
||||
"version": "napi version",
|
||||
"format": "cargo fmt --all --",
|
||||
"lint": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt --all --",
|
||||
"cargo:test": "pnpm run cargo:unit && pnpm run cargo:integration",
|
||||
"cargo:unit": "cargo test unit_test && cargo test -F napi unit_test",
|
||||
"cargo:integration": "cargo test int_test"
|
||||
"version": "napi version"
|
||||
}
|
||||
}
|
||||
|
|
67
packages/backend-rs/src/config/constant.rs
Normal file
67
packages/backend-rs/src/config/constant.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
#[crate::export]
|
||||
pub const SECOND: i32 = 1000;
|
||||
#[crate::export]
|
||||
pub const MINUTE: i32 = 60 * SECOND;
|
||||
#[crate::export]
|
||||
pub const HOUR: i32 = 60 * MINUTE;
|
||||
#[crate::export]
|
||||
pub const DAY: i32 = 24 * HOUR;
|
||||
|
||||
#[crate::export]
|
||||
pub const USER_ONLINE_THRESHOLD: i32 = 10 * MINUTE;
|
||||
#[crate::export]
|
||||
pub const USER_ACTIVE_THRESHOLD: i32 = 3 * DAY;
|
||||
|
||||
/// List of file types allowed to be viewed directly in the browser
|
||||
/// Anything not included here will be responded as application/octet-stream
|
||||
/// SVG is not allowed because it generates XSS <- we need to fix this and later allow it to be viewed directly
|
||||
/// https://github.com/sindresorhus/file-type/blob/main/supported.js
|
||||
/// https://github.com/sindresorhus/file-type/blob/main/core.js
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
|
||||
#[crate::export]
|
||||
pub const FILE_TYPE_BROWSERSAFE: [&str; 41] = [
|
||||
// Images
|
||||
"image/png",
|
||||
"image/gif", // TODO: deprecated, but still used by old posts, new gifs should be converted to webp in the future
|
||||
"image/jpeg",
|
||||
"image/webp", // TODO: make this the default image format
|
||||
"image/apng",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/x-icon",
|
||||
"image/avif", // not as good supported now, but its good to introduce initial support for the future
|
||||
// OggS
|
||||
"audio/opus",
|
||||
"video/ogg",
|
||||
"audio/ogg",
|
||||
"application/ogg",
|
||||
// ISO/IEC base media file format
|
||||
"video/quicktime",
|
||||
"video/mp4", // TODO: we need to check for av1 later
|
||||
"video/vnd.avi", // also av1
|
||||
"audio/mp4",
|
||||
"video/x-m4v",
|
||||
"audio/x-m4a",
|
||||
"video/3gpp",
|
||||
"video/3gpp2",
|
||||
"video/3gp2",
|
||||
"audio/3gpp",
|
||||
"audio/3gpp2",
|
||||
"audio/3gp2",
|
||||
"video/mpeg",
|
||||
"audio/mpeg",
|
||||
"video/webm",
|
||||
"audio/webm",
|
||||
"audio/aac",
|
||||
"audio/x-flac",
|
||||
"audio/flac",
|
||||
"audio/vnd.wave",
|
||||
"audio/mod",
|
||||
"audio/x-mod",
|
||||
"audio/s3m",
|
||||
"audio/x-s3m",
|
||||
"audio/xm",
|
||||
"audio/x-xm",
|
||||
"audio/it",
|
||||
"audio/x-it",
|
||||
];
|
27
packages/backend-rs/src/config/environment.rs
Normal file
27
packages/backend-rs/src/config/environment.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
// FIXME: Are these options used?
|
||||
#[crate::export(object)]
|
||||
pub struct EnvConfig {
|
||||
pub only_queue: bool,
|
||||
pub only_server: bool,
|
||||
pub no_daemons: bool,
|
||||
pub disable_clustering: bool,
|
||||
pub verbose: bool,
|
||||
pub with_log_time: bool,
|
||||
pub slow: bool,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn load_env() -> EnvConfig {
|
||||
let node_env = std::env::var("NODE_ENV").unwrap_or_default().to_lowercase();
|
||||
let is_testing = node_env == "test";
|
||||
|
||||
EnvConfig {
|
||||
only_queue: std::env::var("MK_ONLY_QUEUE").is_ok(),
|
||||
only_server: std::env::var("MK_ONLY_SERVER").is_ok(),
|
||||
no_daemons: is_testing || std::env::var("MK_NO_DAEMONS").is_ok(),
|
||||
disable_clustering: is_testing || std::env::var("MK_DISABLE_CLUSTERING").is_ok(),
|
||||
verbose: std::env::var("MK_VERBOSE").is_ok(),
|
||||
with_log_time: std::env::var("MK_WITH_LOG_TIME").is_ok(),
|
||||
slow: std::env::var("MK_SLOW").is_ok(),
|
||||
}
|
||||
}
|
5
packages/backend-rs/src/config/mod.rs
Normal file
5
packages/backend-rs/src/config/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub use server::CONFIG;
|
||||
|
||||
pub mod constant;
|
||||
pub mod environment;
|
||||
pub mod server;
|
341
packages/backend-rs/src/config/server.rs
Normal file
341
packages/backend-rs/src/config/server.rs
Normal file
|
@ -0,0 +1,341 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
pub const VERSION: &str = macro_rs::read_version_from_package_json!();
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
struct ServerConfig {
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
/// host to listen on
|
||||
pub bind: Option<String>,
|
||||
pub disable_hsts: Option<bool>,
|
||||
|
||||
pub db: DbConfig,
|
||||
pub redis: RedisConfig,
|
||||
pub cache_server: Option<RedisConfig>,
|
||||
|
||||
pub proxy: Option<String>,
|
||||
pub proxy_smtp: Option<String>,
|
||||
pub proxy_bypass_hosts: Option<Vec<String>>,
|
||||
|
||||
pub allowed_private_networks: Option<Vec<String>>,
|
||||
// TODO: i64 -> u64 (NapiValue is not implemented for u64)
|
||||
pub max_file_size: Option<i64>,
|
||||
pub access_log: Option<String>,
|
||||
pub cluster_limits: Option<WorkerConfigInternal>,
|
||||
pub cuid: Option<IdConfig>,
|
||||
pub outgoing_address: Option<String>,
|
||||
|
||||
pub deliver_job_concurrency: Option<u32>,
|
||||
pub inbox_job_concurrency: Option<u32>,
|
||||
pub deliver_job_per_sec: Option<u32>,
|
||||
pub inbox_job_per_sec: Option<u32>,
|
||||
pub deliver_job_max_attempts: Option<u32>,
|
||||
pub inbox_job_max_attempts: Option<u32>,
|
||||
|
||||
/// deprecated
|
||||
pub log_level: Option<Vec<String>>,
|
||||
|
||||
pub max_log_level: Option<String>,
|
||||
|
||||
pub syslog: Option<SysLogConfig>,
|
||||
|
||||
pub proxy_remote_files: Option<bool>,
|
||||
pub media_proxy: Option<String>,
|
||||
pub summaly_proxy_url: Option<String>,
|
||||
|
||||
pub reserved_usernames: Option<Vec<String>>,
|
||||
|
||||
pub max_user_signups: Option<u32>,
|
||||
pub is_managed_hosting: Option<bool>,
|
||||
pub max_note_length: Option<u32>,
|
||||
pub max_caption_length: Option<u32>,
|
||||
|
||||
pub deepl: Option<DeepLConfig>,
|
||||
pub libre_translate: Option<LibreTranslateConfig>,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub object_storage: Option<ObjectStorageConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct DbConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub db: String,
|
||||
pub user: String,
|
||||
pub pass: String,
|
||||
pub disable_cache: Option<bool>,
|
||||
pub extra: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct RedisConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub family: Option<u8>,
|
||||
pub user: Option<String>,
|
||||
pub pass: Option<String>,
|
||||
pub tls: Option<TlsConfig>,
|
||||
#[serde(default)]
|
||||
pub db: u32,
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct TlsConfig {
|
||||
pub host: String,
|
||||
pub reject_unauthorized: bool,
|
||||
}
|
||||
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct WorkerConfig {
|
||||
pub web: u32,
|
||||
pub queue: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct WorkerConfigInternal {
|
||||
pub web: Option<u32>,
|
||||
pub queue: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct IdConfig {
|
||||
pub length: Option<u8>,
|
||||
pub fingerprint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct SysLogConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct DeepLConfig {
|
||||
pub managed: Option<bool>,
|
||||
pub auth_key: Option<String>,
|
||||
pub is_pro: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct LibreTranslateConfig {
|
||||
pub managed: Option<bool>,
|
||||
pub api_url: Option<String>,
|
||||
pub api_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct EmailConfig {
|
||||
pub managed: Option<bool>,
|
||||
pub address: Option<String>,
|
||||
pub host: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub user: Option<String>,
|
||||
pub pass: Option<String>,
|
||||
pub use_implicit_ssl_tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct ObjectStorageConfig {
|
||||
pub managed: Option<bool>,
|
||||
pub base_url: Option<String>,
|
||||
pub bucket: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
pub region: Option<String>,
|
||||
pub access_key: Option<String>,
|
||||
pub secret_key: Option<String>,
|
||||
pub use_ssl: Option<bool>,
|
||||
pub connnect_over_proxy: Option<bool>,
|
||||
pub set_public_read_on_upload: Option<bool>,
|
||||
pub s3_force_path_style: Option<bool>,
|
||||
}
|
||||
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct Config {
|
||||
// ServerConfig (from default.yml)
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
pub bind: Option<String>,
|
||||
pub disable_hsts: Option<bool>,
|
||||
pub db: DbConfig,
|
||||
pub redis: RedisConfig,
|
||||
pub cache_server: Option<RedisConfig>,
|
||||
pub proxy: Option<String>,
|
||||
pub proxy_smtp: Option<String>,
|
||||
pub proxy_bypass_hosts: Option<Vec<String>>,
|
||||
pub allowed_private_networks: Option<Vec<String>>,
|
||||
pub max_file_size: Option<i64>,
|
||||
pub access_log: Option<String>,
|
||||
pub cluster_limits: WorkerConfig,
|
||||
pub cuid: Option<IdConfig>,
|
||||
pub outgoing_address: Option<String>,
|
||||
pub deliver_job_concurrency: Option<u32>,
|
||||
pub inbox_job_concurrency: Option<u32>,
|
||||
pub deliver_job_per_sec: Option<u32>,
|
||||
pub inbox_job_per_sec: Option<u32>,
|
||||
pub deliver_job_max_attempts: Option<u32>,
|
||||
pub inbox_job_max_attempts: Option<u32>,
|
||||
|
||||
/// deprecated
|
||||
pub log_level: Option<Vec<String>>,
|
||||
|
||||
pub max_log_level: Option<String>,
|
||||
pub syslog: Option<SysLogConfig>,
|
||||
pub proxy_remote_files: Option<bool>,
|
||||
pub media_proxy: Option<String>,
|
||||
pub summaly_proxy_url: Option<String>,
|
||||
pub reserved_usernames: Option<Vec<String>>,
|
||||
pub max_user_signups: Option<u32>,
|
||||
pub is_managed_hosting: Option<bool>,
|
||||
pub max_note_length: u32,
|
||||
pub max_caption_length: u32,
|
||||
pub deepl: Option<DeepLConfig>,
|
||||
pub libre_translate: Option<LibreTranslateConfig>,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub object_storage: Option<ObjectStorageConfig>,
|
||||
|
||||
// Mixin
|
||||
pub version: String,
|
||||
pub host: String,
|
||||
pub hostname: String,
|
||||
pub redis_key_prefix: String,
|
||||
pub scheme: String,
|
||||
pub ws_scheme: String,
|
||||
pub api_url: String,
|
||||
pub ws_url: String,
|
||||
pub auth_url: String,
|
||||
pub drive_url: String,
|
||||
pub user_agent: String,
|
||||
}
|
||||
|
||||
fn read_config_file() -> ServerConfig {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let yml = fs::File::open(cwd.join("../../.config/default.yml"))
|
||||
.expect("Failed to open '.config/default.yml'");
|
||||
let mut data: ServerConfig =
|
||||
serde_yaml::from_reader(yml).expect("Failed to parse .config/default.yml");
|
||||
|
||||
data.url = url::Url::parse(&data.url)
|
||||
.expect("Config url is invalid")
|
||||
.origin()
|
||||
.ascii_serialization();
|
||||
|
||||
if data.bind.is_none() {
|
||||
data.bind = std::env::var("BIND").ok()
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn load_config() -> Config {
|
||||
let server_config = read_config_file();
|
||||
let version = VERSION.to_owned();
|
||||
let url = url::Url::parse(&server_config.url).expect("Config url is invalid");
|
||||
let hostname = url
|
||||
.host_str()
|
||||
.expect("Hostname is missing in the config url")
|
||||
.to_owned();
|
||||
let host = match url.port() {
|
||||
Some(port) => format!("{}:{}", hostname, port),
|
||||
None => hostname.clone(),
|
||||
};
|
||||
let scheme = url.scheme().to_owned();
|
||||
let ws_scheme = scheme.replace("http", "ws");
|
||||
|
||||
let cluster_limits = match server_config.cluster_limits {
|
||||
Some(cl) => WorkerConfig {
|
||||
web: cl.web.unwrap_or(1),
|
||||
queue: cl.queue.unwrap_or(1),
|
||||
},
|
||||
None => WorkerConfig { web: 1, queue: 1 },
|
||||
};
|
||||
|
||||
let redis_key_prefix = if let Some(cache_server) = &server_config.cache_server {
|
||||
cache_server.prefix.clone()
|
||||
} else {
|
||||
server_config.redis.prefix.clone()
|
||||
}
|
||||
.unwrap_or(hostname.clone());
|
||||
|
||||
Config {
|
||||
url: server_config.url,
|
||||
port: server_config.port,
|
||||
bind: server_config.bind,
|
||||
disable_hsts: server_config.disable_hsts,
|
||||
db: server_config.db,
|
||||
redis: server_config.redis,
|
||||
cache_server: server_config.cache_server,
|
||||
proxy: server_config.proxy,
|
||||
proxy_smtp: server_config.proxy_smtp,
|
||||
proxy_bypass_hosts: server_config.proxy_bypass_hosts,
|
||||
allowed_private_networks: server_config.allowed_private_networks,
|
||||
max_file_size: server_config.max_file_size,
|
||||
access_log: server_config.access_log,
|
||||
cluster_limits,
|
||||
cuid: server_config.cuid,
|
||||
outgoing_address: server_config.outgoing_address,
|
||||
deliver_job_concurrency: server_config.deliver_job_concurrency,
|
||||
inbox_job_concurrency: server_config.inbox_job_concurrency,
|
||||
deliver_job_per_sec: server_config.deliver_job_per_sec,
|
||||
inbox_job_per_sec: server_config.inbox_job_per_sec,
|
||||
deliver_job_max_attempts: server_config.deliver_job_max_attempts,
|
||||
inbox_job_max_attempts: server_config.inbox_job_max_attempts,
|
||||
log_level: server_config.log_level,
|
||||
max_log_level: server_config.max_log_level,
|
||||
syslog: server_config.syslog,
|
||||
proxy_remote_files: server_config.proxy_remote_files,
|
||||
media_proxy: server_config.media_proxy,
|
||||
summaly_proxy_url: server_config.summaly_proxy_url,
|
||||
reserved_usernames: server_config.reserved_usernames,
|
||||
max_user_signups: server_config.max_user_signups,
|
||||
is_managed_hosting: server_config.is_managed_hosting,
|
||||
max_note_length: server_config.max_note_length.unwrap_or(3000),
|
||||
max_caption_length: server_config.max_caption_length.unwrap_or(1500),
|
||||
deepl: server_config.deepl,
|
||||
libre_translate: server_config.libre_translate,
|
||||
email: server_config.email,
|
||||
object_storage: server_config.object_storage,
|
||||
|
||||
ws_url: format!("{}://{}", ws_scheme, host),
|
||||
api_url: format!("{}://{}/api", scheme, host),
|
||||
auth_url: format!("{}://{}/auth", scheme, host),
|
||||
drive_url: format!("{}://{}/files", scheme, host),
|
||||
user_agent: format!("Firefish/{} ({})", version, url),
|
||||
version,
|
||||
host,
|
||||
hostname,
|
||||
redis_key_prefix,
|
||||
scheme,
|
||||
ws_scheme,
|
||||
}
|
||||
}
|
||||
|
||||
pub static CONFIG: Lazy<Config> = Lazy::new(load_config);
|
308
packages/backend-rs/src/database/cache.rs
Normal file
308
packages/backend-rs/src/database/cache.rs
Normal file
|
@ -0,0 +1,308 @@
|
|||
use crate::database::{redis_conn, redis_key, RedisConnError};
|
||||
use redis::{AsyncCommands, RedisError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(strum::Display, Debug)]
|
||||
pub enum Category {
|
||||
#[strum(serialize = "fetchUrl")]
|
||||
FetchUrl,
|
||||
#[strum(serialize = "blocking")]
|
||||
Block,
|
||||
#[strum(serialize = "following")]
|
||||
Follow,
|
||||
#[cfg(test)]
|
||||
#[strum(serialize = "usedOnlyForTesting")]
|
||||
Test,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisErr(#[from] RedisError),
|
||||
#[error("Redis connection error: {0}")]
|
||||
RedisConnErr(#[from] RedisConnError),
|
||||
#[error("Data serialization error: {0}")]
|
||||
SerializeErr(#[from] rmp_serde::encode::Error),
|
||||
#[error("Data deserialization error: {0}")]
|
||||
DeserializeErr(#[from] rmp_serde::decode::Error),
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn prefix_key(key: &str) -> String {
|
||||
redis_key(format!("cache:{}", key))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn categorize(category: Category, key: &str) -> String {
|
||||
format!("{}:{}", category, key)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wildcard(category: Category) -> String {
|
||||
prefix_key(&categorize(category, "*"))
|
||||
}
|
||||
|
||||
/// Sets a Redis cache.
|
||||
///
|
||||
/// This overwrites the exsisting cache with the same key.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
/// * `value` - (de)serializable value
|
||||
/// * `expire_seconds` - TTL
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use backend_rs::database::cache;
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let key = "apple";
|
||||
/// let data = "I want to cache this string".to_string();
|
||||
///
|
||||
/// // caches the data for 10 seconds
|
||||
/// cache::set(key, &data, 10).await;
|
||||
///
|
||||
/// // get the cache
|
||||
/// let cached_data = cache::get::<String>(key).await.unwrap();
|
||||
/// assert_eq!(data, cached_data.unwrap());
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn set<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
key: &str,
|
||||
value: &V,
|
||||
expire_seconds: u64,
|
||||
) -> Result<(), Error> {
|
||||
redis_conn()
|
||||
.await?
|
||||
.set_ex(
|
||||
prefix_key(key),
|
||||
rmp_serde::encode::to_vec(&value)?,
|
||||
expire_seconds,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a Redis cache.
|
||||
///
|
||||
/// If the Redis connection is fine, this returns `Ok(data)` where `data`
|
||||
/// is the cached value. Returns `Ok(None)` if there is no value corresponding to `key`.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use backend_rs::database::cache;
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let key = "banana";
|
||||
/// let data = "I want to cache this string".to_string();
|
||||
///
|
||||
/// // set cache
|
||||
/// cache::set(key, &data, 10).await.unwrap();
|
||||
///
|
||||
/// // get cache
|
||||
/// let cached_data = cache::get::<String>(key).await.unwrap();
|
||||
/// assert_eq!(data, cached_data.unwrap());
|
||||
///
|
||||
/// // get nonexistent (or expired) cache
|
||||
/// let no_cache = cache::get::<String>("nonexistent").await.unwrap();
|
||||
/// assert!(no_cache.is_none());
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
|
||||
let serialized_value: Option<Vec<u8>> = redis_conn().await?.get(prefix_key(key)).await?;
|
||||
Ok(match serialized_value {
|
||||
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes a Redis cache.
|
||||
///
|
||||
/// If the Redis connection is fine, this returns `Ok(())`
|
||||
/// regardless of whether the cache exists.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use backend_rs::database::cache;
|
||||
/// # tokio_test::block_on(async {
|
||||
/// let key = "chocolate";
|
||||
/// let value = "I want to cache this string".to_string();
|
||||
///
|
||||
/// // set cache
|
||||
/// cache::set(key, &value, 10).await.unwrap();
|
||||
///
|
||||
/// // delete the cache
|
||||
/// cache::delete("foo").await.unwrap();
|
||||
/// cache::delete("nonexistent").await.unwrap(); // this is okay
|
||||
///
|
||||
/// // the cache is gone
|
||||
/// let cached_value = cache::get::<String>("foo").await.unwrap();
|
||||
/// assert!(cached_value.is_none());
|
||||
/// # })
|
||||
/// ```
|
||||
pub async fn delete(key: &str) -> Result<(), Error> {
|
||||
Ok(redis_conn().await?.del(prefix_key(key)).await?)
|
||||
}
|
||||
|
||||
/// Sets a Redis cache under a `category`.
|
||||
///
|
||||
/// The usage is the same as [set], except that you need to
|
||||
/// use [get_one] and [delete_one] to get/delete the cache.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
/// * `value` - (de)serializable value
|
||||
/// * `expire_seconds` - TTL
|
||||
pub async fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
category: Category,
|
||||
key: &str,
|
||||
value: &V,
|
||||
expire_seconds: u64,
|
||||
) -> Result<(), Error> {
|
||||
set(&categorize(category, key), value, expire_seconds).await
|
||||
}
|
||||
|
||||
/// Gets a Redis cache under a `category`.
|
||||
///
|
||||
/// The usage is basically the same as [get].
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
pub async fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
category: Category,
|
||||
key: &str,
|
||||
) -> Result<Option<V>, Error> {
|
||||
get(&categorize(category, key)).await
|
||||
}
|
||||
|
||||
/// Deletes a Redis cache under a `category`.
|
||||
///
|
||||
/// The usage is basically the same as [delete].
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
/// * `key` - key (will be prefixed automatically)
|
||||
pub async fn delete_one(category: Category, key: &str) -> Result<(), Error> {
|
||||
delete(&categorize(category, key)).await
|
||||
}
|
||||
|
||||
/// Deletes all Redis caches under a `category`.
|
||||
///
|
||||
/// ## Arguments
|
||||
///
|
||||
/// * `category` - one of [Category]
|
||||
pub async fn delete_all(category: Category) -> Result<(), Error> {
|
||||
let mut redis = redis_conn().await?;
|
||||
let keys: Vec<Vec<u8>> = redis.keys(wildcard(category)).await?;
|
||||
|
||||
if !keys.is_empty() {
|
||||
redis.del(keys).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: get_all()
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{delete_all, get, get_one, set, set_one, Category::Test};
|
||||
use crate::database::cache::delete_one;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_get_expire() {
|
||||
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
|
||||
struct Data {
|
||||
id: u32,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
let key_1 = "CARGO_TEST_CACHE_KEY_1";
|
||||
let value_1: Vec<i32> = vec![1, 2, 3, 4, 5];
|
||||
|
||||
let key_2 = "CARGO_TEST_CACHE_KEY_2";
|
||||
let value_2 = "Hello fedizens".to_string();
|
||||
|
||||
let key_3 = "CARGO_TEST_CACHE_KEY_3";
|
||||
let value_3 = Data {
|
||||
id: 1000000007,
|
||||
kind: "prime number".to_string(),
|
||||
};
|
||||
|
||||
set(key_1, &value_1, 1).await.unwrap();
|
||||
set(key_2, &value_2, 1).await.unwrap();
|
||||
set(key_3, &value_3, 1).await.unwrap();
|
||||
|
||||
let cached_value_1: Vec<i32> = get(key_1).await.unwrap().unwrap();
|
||||
let cached_value_2: String = get(key_2).await.unwrap().unwrap();
|
||||
let cached_value_3: Data = get(key_3).await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(value_1, cached_value_1);
|
||||
assert_eq!(value_2, cached_value_2);
|
||||
assert_eq!(value_3, cached_value_3);
|
||||
|
||||
// wait for the cache to expire
|
||||
std::thread::sleep(std::time::Duration::from_millis(1100));
|
||||
|
||||
let expired_value_1: Option<Vec<i32>> = get(key_1).await.unwrap();
|
||||
let expired_value_2: Option<Vec<i32>> = get(key_2).await.unwrap();
|
||||
let expired_value_3: Option<Vec<i32>> = get(key_3).await.unwrap();
|
||||
|
||||
assert!(expired_value_1.is_none());
|
||||
assert!(expired_value_2.is_none());
|
||||
assert!(expired_value_3.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn use_category() {
|
||||
let key_1 = "fire";
|
||||
let key_2 = "fish";
|
||||
let key_3 = "awawa";
|
||||
|
||||
let value_1 = "hello".to_string();
|
||||
let value_2 = 998244353u32;
|
||||
let value_3 = 'あ';
|
||||
|
||||
set_one(Test, key_1, &value_1, 5 * 60).await.unwrap();
|
||||
set_one(Test, key_2, &value_2, 5 * 60).await.unwrap();
|
||||
set_one(Test, key_3, &value_3, 5 * 60).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_one::<String>(Test, key_1).await.unwrap().unwrap(),
|
||||
value_1
|
||||
);
|
||||
assert_eq!(get_one::<u32>(Test, key_2).await.unwrap().unwrap(), value_2);
|
||||
assert_eq!(
|
||||
get_one::<char>(Test, key_3).await.unwrap().unwrap(),
|
||||
value_3
|
||||
);
|
||||
|
||||
delete_one(Test, key_1).await.unwrap();
|
||||
|
||||
assert!(get_one::<String>(Test, key_1).await.unwrap().is_none());
|
||||
assert!(get_one::<u32>(Test, key_2).await.unwrap().is_some());
|
||||
assert!(get_one::<char>(Test, key_3).await.unwrap().is_some());
|
||||
|
||||
delete_all(Test).await.unwrap();
|
||||
|
||||
assert!(get_one::<String>(Test, key_1).await.unwrap().is_none());
|
||||
assert!(get_one::<u32>(Test, key_2).await.unwrap().is_none());
|
||||
assert!(get_one::<char>(Test, key_3).await.unwrap().is_none());
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
use sea_orm::error::DbErr;
|
||||
|
||||
use crate::impl_into_napi_error;
|
||||
|
||||
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("The database connections have not been initialized yet")]
|
||||
Uninitialized,
|
||||
#[error("ORM error: {0}")]
|
||||
OrmError(#[from] DbErr),
|
||||
}
|
||||
|
||||
impl_into_napi_error!(Error);
|
|
@ -1,26 +1,8 @@
|
|||
pub mod error;
|
||||
pub use postgresql::db_conn;
|
||||
pub use redis::key as redis_key;
|
||||
pub use redis::redis_conn;
|
||||
pub use redis::RedisConnError;
|
||||
|
||||
use error::Error;
|
||||
use sea_orm::{Database, DbConn};
|
||||
|
||||
static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new();
|
||||
|
||||
pub async fn init_database(conn_uri: impl Into<String>) -> Result<(), Error> {
|
||||
let conn = Database::connect(conn_uri.into()).await?;
|
||||
DB_CONN.get_or_init(move || conn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_database() -> Result<&'static DbConn, Error> {
|
||||
DB_CONN.get().ok_or(Error::Uninitialized)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{error::Error, get_database};
|
||||
|
||||
#[test]
|
||||
fn error_uninitialized() {
|
||||
assert_eq!(get_database().unwrap_err(), Error::Uninitialized);
|
||||
}
|
||||
}
|
||||
pub mod cache;
|
||||
pub mod postgresql;
|
||||
pub mod redis;
|
||||
|
|
43
packages/backend-rs/src/database/postgresql.rs
Normal file
43
packages/backend-rs/src/database/postgresql.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use crate::config::CONFIG;
|
||||
use once_cell::sync::OnceCell;
|
||||
use sea_orm::{ConnectOptions, Database, DbConn, DbErr};
|
||||
use tracing::log::LevelFilter;
|
||||
|
||||
static DB_CONN: OnceCell<DbConn> = OnceCell::new();
|
||||
|
||||
async fn init_database() -> Result<&'static DbConn, DbErr> {
|
||||
let database_uri = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
CONFIG.db.user,
|
||||
urlencoding::encode(&CONFIG.db.pass),
|
||||
CONFIG.db.host,
|
||||
CONFIG.db.port,
|
||||
CONFIG.db.db,
|
||||
);
|
||||
let option: ConnectOptions = ConnectOptions::new(database_uri)
|
||||
.sqlx_logging_level(LevelFilter::Trace)
|
||||
.to_owned();
|
||||
|
||||
tracing::info!("Initializing PostgreSQL connection");
|
||||
|
||||
let conn = Database::connect(option).await?;
|
||||
Ok(DB_CONN.get_or_init(move || conn))
|
||||
}
|
||||
|
||||
pub async fn db_conn() -> Result<&'static DbConn, DbErr> {
|
||||
match DB_CONN.get() {
|
||||
Some(conn) => Ok(conn),
|
||||
None => init_database().await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::db_conn;
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect() {
|
||||
assert!(db_conn().await.is_ok());
|
||||
assert!(db_conn().await.is_ok());
|
||||
}
|
||||
}
|
138
packages/backend-rs/src/database/redis.rs
Normal file
138
packages/backend-rs/src/database/redis.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
use crate::config::CONFIG;
|
||||
use async_trait::async_trait;
|
||||
use bb8::{ManageConnection, Pool, PooledConnection, RunError};
|
||||
use redis::{aio::MultiplexedConnection, Client, ErrorKind, IntoConnectionInfo, RedisError};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
/// A `bb8::ManageConnection` for `redis::Client::get_multiplexed_async_connection`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RedisConnectionManager {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl RedisConnectionManager {
|
||||
/// Create a new `RedisConnectionManager`.
|
||||
/// See `redis::Client::open` for a description of the parameter types.
|
||||
pub fn new<T: IntoConnectionInfo>(info: T) -> Result<Self, RedisError> {
|
||||
Ok(Self {
|
||||
client: Client::open(info.into_connection_info()?)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ManageConnection for RedisConnectionManager {
|
||||
type Connection = MultiplexedConnection;
|
||||
type Error = RedisError;
|
||||
|
||||
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
|
||||
self.client.get_multiplexed_async_connection().await
|
||||
}
|
||||
|
||||
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
|
||||
let pong: String = redis::cmd("PING").query_async(conn).await?;
|
||||
match pong.as_str() {
|
||||
"PONG" => Ok(()),
|
||||
_ => Err((ErrorKind::ResponseError, "ping request").into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_broken(&self, _: &mut Self::Connection) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
static CONN_POOL: OnceCell<Pool<RedisConnectionManager>> = OnceCell::const_new();
|
||||
|
||||
async fn init_conn_pool() -> Result<(), RedisError> {
|
||||
let redis_url = {
|
||||
let mut params = vec!["redis://".to_owned()];
|
||||
|
||||
let redis = if let Some(cache_server) = &CONFIG.cache_server {
|
||||
cache_server
|
||||
} else {
|
||||
&CONFIG.redis
|
||||
};
|
||||
|
||||
if let Some(user) = &redis.user {
|
||||
params.push(user.to_string())
|
||||
}
|
||||
if let Some(pass) = &redis.pass {
|
||||
params.push(format!(":{}@", pass))
|
||||
}
|
||||
params.push(redis.host.to_string());
|
||||
params.push(format!(":{}", redis.port));
|
||||
params.push(format!("/{}", redis.db));
|
||||
|
||||
params.concat()
|
||||
};
|
||||
|
||||
tracing::info!("Initializing connection manager");
|
||||
let manager = RedisConnectionManager::new(redis_url)?;
|
||||
|
||||
tracing::info!("Creating connection pool");
|
||||
let pool = Pool::builder().build(manager).await?;
|
||||
|
||||
CONN_POOL.get_or_init(|| async { pool }).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RedisConnError {
|
||||
#[error("Failed to initialize Redis connection pool: {0}")]
|
||||
RedisErr(RedisError),
|
||||
#[error("Redis connection pool error: {0}")]
|
||||
Bb8PoolErr(RunError<RedisError>),
|
||||
}
|
||||
|
||||
pub async fn redis_conn(
|
||||
) -> Result<PooledConnection<'static, RedisConnectionManager>, RedisConnError> {
|
||||
if !CONN_POOL.initialized() {
|
||||
let init_res = init_conn_pool().await;
|
||||
|
||||
if let Err(err) = init_res {
|
||||
return Err(RedisConnError::RedisErr(err));
|
||||
}
|
||||
}
|
||||
|
||||
CONN_POOL
|
||||
.get()
|
||||
.unwrap()
|
||||
.get()
|
||||
.await
|
||||
.map_err(RedisConnError::Bb8PoolErr)
|
||||
}
|
||||
|
||||
/// prefix redis key
|
||||
#[inline]
|
||||
pub fn key(key: impl ToString) -> String {
|
||||
format!("{}:{}", CONFIG.redis_key_prefix, key.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::redis_conn;
|
||||
use pretty_assertions::assert_eq;
|
||||
use redis::AsyncCommands;
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect() {
|
||||
assert!(redis_conn().await.is_ok());
|
||||
assert!(redis_conn().await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn access() {
|
||||
let mut redis = redis_conn().await.unwrap();
|
||||
|
||||
let key = "CARGO_UNIT_TEST_KEY";
|
||||
let value = "CARGO_UNIT_TEST_VALUE";
|
||||
|
||||
assert_eq!(
|
||||
redis.set::<&str, &str, String>(key, value).await.unwrap(),
|
||||
"OK"
|
||||
);
|
||||
assert_eq!(redis.get::<&str, String>(key).await.unwrap(), value);
|
||||
assert_eq!(redis.del::<&str, u32>(key).await.unwrap(), 1);
|
||||
}
|
||||
}
|
105
packages/backend-rs/src/federation/acct.rs
Normal file
105
packages/backend-rs/src/federation/acct.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[crate::export(object)]
|
||||
pub struct Acct {
|
||||
pub username: String,
|
||||
pub host: Option<String>,
|
||||
}
|
||||
|
||||
impl FromStr for Acct {
|
||||
type Err = ();
|
||||
|
||||
/// This never throw errors. Feel free to `.unwrap()` the result.
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let split: Vec<&str> = if let Some(stripped) = value.strip_prefix('@') {
|
||||
stripped
|
||||
} else {
|
||||
value
|
||||
}
|
||||
.split('@')
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
username: split[0].to_string(),
|
||||
host: if split.len() == 1 {
|
||||
None
|
||||
} else {
|
||||
Some(split[1].to_string())
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Acct {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let result = match &self.host {
|
||||
Some(host) => format!("{}@{}", self.username, host),
|
||||
None => self.username.clone(),
|
||||
};
|
||||
write!(f, "{result}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Acct> for String {
|
||||
fn from(value: Acct) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[crate::ts_only_warn("Use `acct.parse().unwrap()` or `Acct::from_str(acct).unwrap()` instead.")]
|
||||
#[crate::export]
|
||||
pub fn string_to_acct(acct: &str) -> Acct {
|
||||
Acct::from_str(acct).unwrap()
|
||||
}
|
||||
|
||||
#[crate::ts_only_warn("Use `acct.to_string()` instead.")]
|
||||
#[crate::export]
|
||||
pub fn acct_to_string(acct: &Acct) -> String {
|
||||
acct.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::Acct;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_acct_to_string() {
|
||||
let remote_acct = Acct {
|
||||
username: "firefish".to_string(),
|
||||
host: Some("example.com".to_string()),
|
||||
};
|
||||
let local_acct = Acct {
|
||||
username: "MisakaMikoto".to_string(),
|
||||
host: None,
|
||||
};
|
||||
|
||||
assert_eq!(remote_acct.to_string(), "firefish@example.com");
|
||||
assert_ne!(remote_acct.to_string(), "mastodon@example.com");
|
||||
assert_eq!(local_acct.to_string(), "MisakaMikoto");
|
||||
assert_ne!(local_acct.to_string(), "ShiraiKuroko");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_to_acct() {
|
||||
let remote_acct = Acct {
|
||||
username: "firefish".to_string(),
|
||||
host: Some("example.com".to_string()),
|
||||
};
|
||||
let local_acct = Acct {
|
||||
username: "MisakaMikoto".to_string(),
|
||||
host: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Acct::from_str("@firefish@example.com").unwrap(),
|
||||
remote_acct
|
||||
);
|
||||
assert_eq!(Acct::from_str("firefish@example.com").unwrap(), remote_acct);
|
||||
assert_eq!(Acct::from_str("@MisakaMikoto").unwrap(), local_acct);
|
||||
assert_eq!(Acct::from_str("MisakaMikoto").unwrap(), local_acct);
|
||||
}
|
||||
}
|
1
packages/backend-rs/src/federation/mod.rs
Normal file
1
packages/backend-rs/src/federation/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod acct;
|
20
packages/backend-rs/src/init/greet.rs
Normal file
20
packages/backend-rs/src/init/greet.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use crate::config::server::VERSION;
|
||||
|
||||
const GREETING_MESSAGE: &str = "\
|
||||
███████╗██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄
|
||||
██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄
|
||||
█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄
|
||||
██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄
|
||||
██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀
|
||||
Firefish is an open-source decentralized microblogging platform.
|
||||
If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish
|
||||
";
|
||||
|
||||
#[crate::export]
|
||||
pub fn greet() {
|
||||
println!("{}", GREETING_MESSAGE);
|
||||
|
||||
tracing::info!("Welcome to Firefish!");
|
||||
tracing::info!("Firefish {VERSION}");
|
||||
}
|
51
packages/backend-rs/src/init/log.rs
Normal file
51
packages/backend-rs/src/init/log.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use crate::config::CONFIG;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[crate::export(js_name = "initializeRustLogger")]
|
||||
pub fn initialize_logger() {
|
||||
let mut builder = FmtSubscriber::builder();
|
||||
|
||||
if let Some(max_level) = &CONFIG.max_log_level {
|
||||
builder = builder.with_max_level(match max_level.as_str() {
|
||||
"error" => Level::ERROR,
|
||||
"warning" => Level::WARN,
|
||||
"info" => Level::INFO,
|
||||
"debug" => Level::DEBUG,
|
||||
"trace" => Level::TRACE,
|
||||
_ => Level::INFO, // Fallback
|
||||
});
|
||||
} else if let Some(levels) = &CONFIG.log_level {
|
||||
// `logLevel` config is Deprecated
|
||||
if levels.contains(&"trace".to_string()) {
|
||||
builder = builder.with_max_level(Level::TRACE);
|
||||
} else if levels.contains(&"debug".to_string()) {
|
||||
builder = builder.with_max_level(Level::DEBUG);
|
||||
} else if levels.contains(&"info".to_string()) {
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
} else if levels.contains(&"warning".to_string()) {
|
||||
builder = builder.with_max_level(Level::WARN);
|
||||
} else if levels.contains(&"error".to_string()) {
|
||||
builder = builder.with_max_level(Level::ERROR);
|
||||
} else {
|
||||
// Fallback
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
}
|
||||
} else {
|
||||
// Fallback
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
};
|
||||
|
||||
let subscriber = builder
|
||||
.without_time()
|
||||
.with_level(true)
|
||||
.with_ansi(true)
|
||||
.with_target(true)
|
||||
.with_thread_names(true)
|
||||
.with_line_number(true)
|
||||
.log_internal_errors(true)
|
||||
.compact()
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("Failed to initialize the logger");
|
||||
}
|
3
packages/backend-rs/src/init/mod.rs
Normal file
3
packages/backend-rs/src/init/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod greet;
|
||||
pub mod log;
|
||||
pub mod system_info;
|
39
packages/backend-rs/src/init/system_info.rs
Normal file
39
packages/backend-rs/src/init/system_info.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
|
||||
use sysinfo::System;
|
||||
|
||||
pub type SysinfoPoisonError = PoisonError<MutexGuard<'static, System>>;
|
||||
|
||||
// TODO: handle this in a more proper way when we move the entry point to backend-rs
|
||||
pub fn system() -> Result<MutexGuard<'static, System>, SysinfoPoisonError> {
|
||||
pub static SYSTEM: OnceLock<Mutex<System>> = OnceLock::new();
|
||||
SYSTEM.get_or_init(|| Mutex::new(System::new_all())).lock()
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn show_server_info() -> Result<(), SysinfoPoisonError> {
|
||||
let system_info = system()?;
|
||||
|
||||
tracing::info!(
|
||||
"Hostname: {}",
|
||||
System::host_name().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"OS: {}",
|
||||
System::long_os_version().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"Kernel: {}",
|
||||
System::kernel_version().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"CPU architecture: {}",
|
||||
System::cpu_arch().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!("CPU threads: {}", system_info.cpus().len());
|
||||
tracing::info!("Total memory: {} MiB", system_info.total_memory() / 1048576);
|
||||
tracing::info!("Free memory: {} MiB", system_info.free_memory() / 1048576);
|
||||
tracing::info!("Total swap: {} MiB", system_info.total_swap() / 1048576);
|
||||
tracing::info!("Free swap: {} MiB", system_info.free_swap() / 1048576);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
pub mod database;
|
||||
pub mod macros;
|
||||
pub mod model;
|
||||
pub mod util;
|
||||
pub use macro_rs::{export, ts_only_warn};
|
||||
|
||||
#[cfg(feature = "napi")]
|
||||
pub mod mastodon_api;
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod federation;
|
||||
pub mod init;
|
||||
pub mod misc;
|
||||
pub mod model;
|
||||
pub mod service;
|
||||
pub mod util;
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
#[macro_export]
|
||||
macro_rules! impl_into_napi_error {
|
||||
($a:ty) => {
|
||||
#[cfg(feature = "napi")]
|
||||
impl Into<napi::Error> for $a {
|
||||
fn into(self) -> napi::Error {
|
||||
napi::Error::from_reason(self.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
use napi::{Error, Status};
|
||||
use napi_derive::napi;
|
||||
|
||||
static CHAR_COLLECTION: &str = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
// -- NAPI exports --
|
||||
|
||||
#[napi]
|
||||
pub enum IdConvertType {
|
||||
MastodonId,
|
||||
FirefishId,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn convert_id(in_id: String, id_convert_type: IdConvertType) -> napi::Result<String> {
|
||||
use IdConvertType::*;
|
||||
match id_convert_type {
|
||||
MastodonId => {
|
||||
let mut out: i128 = 0;
|
||||
for (i, c) in in_id.to_lowercase().chars().rev().enumerate() {
|
||||
out += num_from_char(c)? as i128 * 36_i128.pow(i as u32);
|
||||
}
|
||||
|
||||
Ok(out.to_string())
|
||||
}
|
||||
FirefishId => {
|
||||
let mut input: i128 = match in_id.parse() {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"Unable to parse ID as MastodonId",
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut out = String::new();
|
||||
|
||||
while input != 0 {
|
||||
out.insert(0, char_from_num((input % 36) as u8)?);
|
||||
input /= 36;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- end --
|
||||
|
||||
#[inline(always)]
|
||||
fn num_from_char(character: char) -> napi::Result<u8> {
|
||||
for (i, c) in CHAR_COLLECTION.chars().enumerate() {
|
||||
if c == character {
|
||||
return Ok(i as u8);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"Invalid character in parsed base36 id",
|
||||
))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn char_from_num(number: u8) -> napi::Result<char> {
|
||||
CHAR_COLLECTION
|
||||
.chars()
|
||||
.nth(number as usize)
|
||||
.ok_or(Error::from_status(Status::Unknown))
|
||||
}
|
175
packages/backend-rs/src/misc/check_hit_antenna.rs
Normal file
175
packages/backend-rs/src/misc/check_hit_antenna.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
use crate::config::CONFIG;
|
||||
use crate::database::{cache, db_conn};
|
||||
use crate::federation::acct::Acct;
|
||||
use crate::misc::get_note_all_texts::{all_texts, NoteLike};
|
||||
use crate::model::entity::{antenna, blocking, following, note, sea_orm_active_enums::*};
|
||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AntennaCheckError {
|
||||
#[error("Database error: {0}")]
|
||||
DbErr(#[from] DbErr),
|
||||
#[error("Cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("User profile not found: {0}")]
|
||||
UserProfileNotFoundErr(String),
|
||||
}
|
||||
|
||||
fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> bool {
|
||||
if case_sensitive {
|
||||
space_separated_words
|
||||
.split_whitespace()
|
||||
.all(|word| text.contains(word))
|
||||
} else {
|
||||
space_separated_words
|
||||
.to_lowercase()
|
||||
.split_whitespace()
|
||||
.all(|word| text.to_lowercase().contains(word))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn check_hit_antenna(
|
||||
antenna: &antenna::Model,
|
||||
note: note::Model,
|
||||
note_author: &Acct,
|
||||
) -> Result<bool, AntennaCheckError> {
|
||||
if note.visibility == NoteVisibilityEnum::Specified {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if antenna.with_file && note.file_ids.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !antenna.with_replies && note.reply_id.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if antenna.src == AntennaSrcEnum::Users {
|
||||
let is_from_one_of_specified_authors = antenna
|
||||
.users
|
||||
.iter()
|
||||
.map(|s| s.parse::<Acct>().unwrap())
|
||||
.any(|acct| acct.username == note_author.username && acct.host == note_author.host);
|
||||
|
||||
if !is_from_one_of_specified_authors {
|
||||
return Ok(false);
|
||||
}
|
||||
} else if antenna.src == AntennaSrcEnum::Instances {
|
||||
let is_from_one_of_specified_servers = antenna.instances.iter().any(|host| {
|
||||
host.to_ascii_lowercase()
|
||||
== note_author
|
||||
.host
|
||||
.clone()
|
||||
.unwrap_or(CONFIG.host.clone())
|
||||
.to_ascii_lowercase()
|
||||
});
|
||||
|
||||
if !is_from_one_of_specified_servers {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// "Home", "Group", "List" sources are currently disabled
|
||||
|
||||
let note_texts = all_texts(NoteLike {
|
||||
file_ids: note.file_ids,
|
||||
user_id: note.user_id.clone(),
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
renote_id: note.renote_id,
|
||||
reply_id: note.reply_id,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let has_keyword = antenna.keywords.iter().any(|words| {
|
||||
note_texts
|
||||
.iter()
|
||||
.any(|text| match_all(words, text, antenna.case_sensitive))
|
||||
});
|
||||
|
||||
if !has_keyword {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let has_excluded_word = antenna.exclude_keywords.iter().any(|words| {
|
||||
note_texts
|
||||
.iter()
|
||||
.any(|text| match_all(words, text, antenna.case_sensitive))
|
||||
});
|
||||
|
||||
if has_excluded_word {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let db = db_conn().await?;
|
||||
|
||||
let blocked_user_ids: Vec<String> = cache::get_one(cache::Category::Block, ¬e.user_id)
|
||||
.await?
|
||||
.unwrap_or({
|
||||
// cache miss
|
||||
let blocks = blocking::Entity::find()
|
||||
.select_only()
|
||||
.column(blocking::Column::BlockeeId)
|
||||
.filter(blocking::Column::BlockerId.eq(¬e.user_id))
|
||||
.into_tuple::<String>()
|
||||
.all(db)
|
||||
.await?;
|
||||
cache::set_one(cache::Category::Block, ¬e.user_id, &blocks, 10 * 60).await?;
|
||||
blocks
|
||||
});
|
||||
|
||||
// if the antenna owner is blocked by the note author, return false
|
||||
if blocked_user_ids.contains(&antenna.user_id) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if [NoteVisibilityEnum::Home, NoteVisibilityEnum::Followers].contains(¬e.visibility) {
|
||||
let following_user_ids: Vec<String> =
|
||||
cache::get_one(cache::Category::Follow, &antenna.user_id)
|
||||
.await?
|
||||
.unwrap_or({
|
||||
// cache miss
|
||||
let following = following::Entity::find()
|
||||
.select_only()
|
||||
.column(following::Column::FolloweeId)
|
||||
.filter(following::Column::FollowerId.eq(&antenna.user_id))
|
||||
.into_tuple::<String>()
|
||||
.all(db)
|
||||
.await?;
|
||||
cache::set_one(
|
||||
cache::Category::Follow,
|
||||
&antenna.user_id,
|
||||
&following,
|
||||
10 * 60,
|
||||
)
|
||||
.await?;
|
||||
following
|
||||
});
|
||||
|
||||
// if the antenna owner is not following the note author, return false
|
||||
if !following_user_ids.contains(¬e.user_id) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::match_all;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_match_all() {
|
||||
assert_eq!(match_all("Apple", "apple and banana", false), true);
|
||||
assert_eq!(match_all("Apple", "apple and banana", true), false);
|
||||
assert_eq!(match_all("Apple Banana", "apple and banana", false), true);
|
||||
assert_eq!(match_all("Apple Banana", "apple and cinnamon", true), false);
|
||||
assert_eq!(
|
||||
match_all("Apple Banana", "apple and cinnamon", false),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
50
packages/backend-rs/src/misc/check_server_block.rs
Normal file
50
packages/backend-rs/src/misc/check_server_block.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use crate::misc::meta::fetch_meta;
|
||||
use sea_orm::DbErr;
|
||||
|
||||
/// Checks if a server is blocked.
|
||||
///
|
||||
/// ## Argument
|
||||
/// `host` - punycoded instance host
|
||||
#[crate::export]
|
||||
pub async fn is_blocked_server(host: &str) -> Result<bool, DbErr> {
|
||||
Ok(fetch_meta(true)
|
||||
.await?
|
||||
.blocked_hosts
|
||||
.iter()
|
||||
.any(|blocked_host| {
|
||||
host == blocked_host || host.ends_with(format!(".{}", blocked_host).as_str())
|
||||
}))
|
||||
}
|
||||
|
||||
/// Checks if a server is silenced.
|
||||
///
|
||||
/// ## Argument
|
||||
/// `host` - punycoded instance host
|
||||
#[crate::export]
|
||||
pub async fn is_silenced_server(host: &str) -> Result<bool, DbErr> {
|
||||
Ok(fetch_meta(true)
|
||||
.await?
|
||||
.silenced_hosts
|
||||
.iter()
|
||||
.any(|silenced_host| {
|
||||
host == silenced_host || host.ends_with(format!(".{}", silenced_host).as_str())
|
||||
}))
|
||||
}
|
||||
|
||||
/// Checks if a server is allowlisted.
|
||||
/// Returns `Ok(true)` if private mode is disabled.
|
||||
///
|
||||
/// ## Argument
|
||||
/// `host` - punycoded instance host
|
||||
#[crate::export]
|
||||
pub async fn is_allowed_server(host: &str) -> Result<bool, DbErr> {
|
||||
let meta = fetch_meta(true).await?;
|
||||
|
||||
if !meta.private_mode.unwrap_or(false) {
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(allowed_hosts) = meta.allowed_hosts {
|
||||
return Ok(allowed_hosts.contains(&host.to_string()));
|
||||
}
|
||||
Ok(false)
|
||||
}
|
166
packages/backend-rs/src/misc/check_word_mute.rs
Normal file
166
packages/backend-rs/src/misc/check_word_mute.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use crate::misc::get_note_all_texts::{all_texts, NoteLike};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sea_orm::DbErr;
|
||||
|
||||
fn convert_regex(js_regex: &str) -> String {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap());
|
||||
RE.replace(js_regex, "(?$2)$1").to_string()
|
||||
}
|
||||
|
||||
fn check_word_mute_impl(
|
||||
texts: &[String],
|
||||
muted_words: &[String],
|
||||
muted_patterns: &[String],
|
||||
) -> bool {
|
||||
muted_words.iter().any(|item| {
|
||||
texts.iter().any(|text| {
|
||||
let text_lower = text.to_lowercase();
|
||||
item.split_whitespace()
|
||||
.all(|muted_word| text_lower.contains(&muted_word.to_lowercase()))
|
||||
})
|
||||
}) || muted_patterns.iter().any(|muted_pattern| {
|
||||
Regex::new(convert_regex(muted_pattern).as_str())
|
||||
.map(|re| texts.iter().any(|text| re.is_match(text)))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn check_word_mute(
|
||||
note: NoteLike,
|
||||
muted_words: &[String],
|
||||
muted_patterns: &[String],
|
||||
) -> Result<bool, DbErr> {
|
||||
if muted_words.is_empty() && muted_patterns.is_empty() {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(check_word_mute_impl(
|
||||
&all_texts(note).await?,
|
||||
muted_words,
|
||||
muted_patterns,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::check_word_mute_impl;
|
||||
|
||||
#[test]
|
||||
fn test_word_mute_match() {
|
||||
let texts = vec![
|
||||
"The quick brown fox jumps over the lazy dog.".to_string(),
|
||||
"色は匂へど 散りぬるを 我が世誰ぞ 常ならむ".to_string(),
|
||||
"😇".to_string(),
|
||||
];
|
||||
|
||||
let hiragana_1 = r#"/[\u{3040}-\u{309f}]/u"#.to_string();
|
||||
let hiragana_2 = r#"/[あ-ん]/u"#.to_string();
|
||||
let katakana_1 = r#"/[\u{30a1}-\u{30ff}]/u"#.to_string();
|
||||
let katakana_2 = r#"/[ア-ン]/u"#.to_string();
|
||||
let emoji = r#"/[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/u"#.to_string();
|
||||
|
||||
assert!(check_word_mute_impl(&texts, &[], &["/the/i".to_string()]));
|
||||
|
||||
assert!(!check_word_mute_impl(&texts, &[], &["/the/".to_string()]));
|
||||
|
||||
assert!(check_word_mute_impl(&texts, &[], &["/QuICk/i".to_string()]));
|
||||
|
||||
assert!(!check_word_mute_impl(&texts, &[], &["/QuICk/".to_string()]));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&[
|
||||
"我".to_string(),
|
||||
"有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()
|
||||
],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&[
|
||||
"有為の奥山".to_string(),
|
||||
"今日越えて".to_string(),
|
||||
"浅き夢見し".to_string(),
|
||||
"酔ひもせず".to_string()
|
||||
],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&[hiragana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&[hiragana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&[katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&[katakana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["brown fox".to_string(), "mastodon".to_string()],
|
||||
&[katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["brown fox".to_string(), "mastodon".to_string()],
|
||||
&[katakana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "dog".to_string()],
|
||||
&[katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "dog".to_string()],
|
||||
&[katakana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&[hiragana_1.clone(), katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["😇".to_string(), "🥲".to_string(), "🥴".to_string()],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["🙂".to_string(), "🥲".to_string(), "🥴".to_string()],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(&texts, &[], &[emoji.clone()]));
|
||||
}
|
||||
}
|
67
packages/backend-rs/src/misc/convert_host.rs
Normal file
67
packages/backend-rs/src/misc/convert_host.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
use crate::config::CONFIG;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Idna error: {0}")]
|
||||
IdnaError(#[from] idna::Errors),
|
||||
#[error("Url parse error: {0}")]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
#[error("Hostname is missing")]
|
||||
NoHostname,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn get_full_ap_account(username: &str, host: Option<&str>) -> Result<String, Error> {
|
||||
Ok(match host {
|
||||
Some(host) => format!("{}@{}", username, to_puny(host)?),
|
||||
None => format!("{}@{}", username, extract_host(&CONFIG.url)?),
|
||||
})
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn is_self_host(host: Option<&str>) -> Result<bool, Error> {
|
||||
Ok(match host {
|
||||
Some(host) => extract_host(&CONFIG.url)? == to_puny(host)?,
|
||||
None => true,
|
||||
})
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn is_same_origin(uri: &str) -> Result<bool, Error> {
|
||||
Ok(url::Url::parse(uri)?.origin().ascii_serialization() == CONFIG.url)
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn extract_host(uri: &str) -> Result<String, Error> {
|
||||
url::Url::parse(uri)?
|
||||
.host_str()
|
||||
.ok_or(Error::NoHostname)
|
||||
.and_then(|v| Ok(to_puny(v)?))
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn to_puny(host: &str) -> Result<String, idna::Errors> {
|
||||
idna::domain_to_ascii(host)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{extract_host, to_puny};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn extract_host_test() {
|
||||
assert_eq!(
|
||||
extract_host("https://firefish.dev/firefish/firefish.git").unwrap(),
|
||||
"firefish.dev"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_puny_test() {
|
||||
assert_eq!(
|
||||
to_puny("何もかも.owari.shop").unwrap(),
|
||||
"xn--u8jyfb5762a.owari.shop"
|
||||
);
|
||||
}
|
||||
}
|
31
packages/backend-rs/src/misc/emoji.rs
Normal file
31
packages/backend-rs/src/misc/emoji.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
#[inline]
|
||||
#[crate::export]
|
||||
pub fn is_unicode_emoji(s: &str) -> bool {
|
||||
emojis::get(s).is_some()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::is_unicode_emoji;
|
||||
|
||||
#[test]
|
||||
fn test_unicode_emoji_check() {
|
||||
assert!(is_unicode_emoji("⭐"));
|
||||
assert!(is_unicode_emoji("👍"));
|
||||
assert!(is_unicode_emoji("❤"));
|
||||
assert!(is_unicode_emoji("♥️"));
|
||||
assert!(is_unicode_emoji("❤️"));
|
||||
assert!(is_unicode_emoji("💙"));
|
||||
assert!(is_unicode_emoji("🩷"));
|
||||
assert!(is_unicode_emoji("🖖🏿"));
|
||||
assert!(is_unicode_emoji("🏃➡️"));
|
||||
assert!(is_unicode_emoji("👩❤️👨"));
|
||||
assert!(is_unicode_emoji("👩👦👦"));
|
||||
assert!(is_unicode_emoji("🏳️🌈"));
|
||||
|
||||
assert!(!is_unicode_emoji("⭐⭐"));
|
||||
assert!(!is_unicode_emoji("x"));
|
||||
assert!(!is_unicode_emoji("\t"));
|
||||
assert!(!is_unicode_emoji(":meow_aww:"));
|
||||
}
|
||||
}
|
36
packages/backend-rs/src/misc/escape_sql.rs
Normal file
36
packages/backend-rs/src/misc/escape_sql.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
#[crate::export]
|
||||
pub fn sql_like_escape(src: &str) -> String {
|
||||
src.replace('%', r"\%").replace('_', r"\_")
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn safe_for_sql(src: &str) -> bool {
|
||||
!src.contains([
|
||||
'\0', '\x08', '\x09', '\x1a', '\n', '\r', '"', '\'', '\\', '%',
|
||||
])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{safe_for_sql, sql_like_escape};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn sql_like_escape_test() {
|
||||
assert_eq!(sql_like_escape(""), "");
|
||||
assert_eq!(sql_like_escape("abc"), "abc");
|
||||
assert_eq!(sql_like_escape("a%bc"), r"a\%bc");
|
||||
assert_eq!(sql_like_escape("a呼%吸bc"), r"a呼\%吸bc");
|
||||
assert_eq!(sql_like_escape("a呼%吸b%_c"), r"a呼\%吸b\%\_c");
|
||||
assert_eq!(sql_like_escape("_اللغة العربية"), r"\_اللغة العربية");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safe_for_sql_test() {
|
||||
assert!(safe_for_sql("123"));
|
||||
assert!(safe_for_sql("人間"));
|
||||
assert!(!safe_for_sql("人間\x09"));
|
||||
assert!(!safe_for_sql("abc\ndef"));
|
||||
assert!(!safe_for_sql("%something%"));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue