mirror of
https://git.joinfirefish.org/firefish/firefish.git
synced 2024-05-18 09:41:12 +02:00
Compare commits
701 commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
bb9a58ce34 | |||
baba86a203 | |||
f95e2d1c51 | |||
2b88ef18a5 | |||
5eff4da27b | |||
f44a2937d4 | |||
393ab2590d | |||
f422842aef | |||
695cb452bc | |||
e4927c9b9b | |||
ea6ef881c2 | |||
2bf51abc12 | |||
5da03666b2 | |||
f3b189a70c | |||
95b91c6396 | |||
9c75dd0b26 | |||
38668c4c11 | |||
e6b7eca775 | |||
8e28f0e97c | |||
d8b4eb6f5e | |||
ef94ba1474 | |||
c4a093209f | |||
db37eb4ad1 | |||
3716e7f74c | |||
7a4e6334f1 | |||
783f5481bb | |||
3ddd68097a | |||
f275fc9cdf | |||
18ba024cbb | |||
43aeec32ce | |||
909125e519 | |||
124b2244d6 | |||
0b7ab1b90d | |||
6982843716 | |||
17a945b8b1 | |||
f7f7959ba6 | |||
c1155f169a | |||
b59186f093 | |||
1a1d817772 | |||
73537ec6fa | |||
6ac6a4cfa9 | |||
b6baded2e3 | |||
23145c61af | |||
268b7aeb3f | |||
46bf02cdd5 | |||
f346d2e2f6 | |||
2267e90d3b | |||
db0bd21edc | |||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
267
.gitlab-ci.yml
Normal file
267
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,267 @@
|
|||
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_PIPELINE_SOURCE == 'merge_request_event'
|
||||
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_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
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_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
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: docker.io/debian:bookworm-slim
|
||||
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
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
|
||||
- buildah prune --all
|
||||
- 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 \
|
||||
--isolation chroot \
|
||||
--device /dev/fuse:rw \
|
||||
--security-opt seccomp=unconfined \
|
||||
--security-opt apparmor=unconfined \
|
||||
--cap-add all \
|
||||
--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_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: always
|
||||
script:
|
||||
- curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin
|
||||
- pnpm install --frozen-lockfile
|
||||
- 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
|
||||
- pnpm --filter='!backend-rs' run build:debug
|
||||
- cargo test --doc
|
||||
- cargo nextest run
|
||||
|
||||
cargo:clippy:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
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,30 +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)
|
||||
## 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,18 +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)
|
||||
## 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,8 +1,9 @@
|
|||
<!-- 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
|
||||
|
|
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"
|
||||
]
|
||||
}
|
||||
|
|
8
COPYING
8
COPYING
|
@ -26,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
|
||||
|
|
2313
Cargo.lock
generated
2313
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
38
Cargo.toml
38
Cargo.toml
|
@ -5,35 +5,47 @@ resolver = "2"
|
|||
[workspace.dependencies]
|
||||
macro_rs = { path = "packages/macro-rs" }
|
||||
|
||||
napi = { version = "2.16.2", default-features = false }
|
||||
napi-derive = "2.16.2"
|
||||
napi-build = "2.1.2"
|
||||
napi = { version = "2.16.6", default-features = false }
|
||||
napi-derive = "2.16.4"
|
||||
napi-build = "2.1.3"
|
||||
|
||||
argon2 = "0.5.3"
|
||||
async-trait = "0.1.80"
|
||||
basen = "0.1.0"
|
||||
cfg-if = "1.0.0"
|
||||
chrono = "0.4.37"
|
||||
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"
|
||||
jsonschema = "0.17.1"
|
||||
image = "0.25.1"
|
||||
isahc = "1.7.2"
|
||||
nom-exif = "1.2.0"
|
||||
once_cell = "1.19.0"
|
||||
parse-display = "0.9.0"
|
||||
openssl = "0.10.64"
|
||||
pretty_assertions = "1.4.0"
|
||||
proc-macro2 = "1.0.79"
|
||||
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"
|
||||
schemars = "0.8.16"
|
||||
rmp-serde = "1.3.0"
|
||||
sea-orm = "0.12.15"
|
||||
serde = "1.0.197"
|
||||
serde_json = "1.0.115"
|
||||
serde = "1.0.202"
|
||||
serde_json = "1.0.117"
|
||||
serde_yaml = "0.9.34"
|
||||
syn = "2.0.58"
|
||||
thiserror = "1.0.58"
|
||||
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
|
||||
|
|
46
Dockerfile
46
Dockerfile
|
@ -3,11 +3,16 @@ 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 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
|
||||
|
||||
COPY Cargo.toml Cargo.toml
|
||||
COPY Cargo.lock Cargo.lock
|
||||
COPY packages/backend-rs/Cargo.toml packages/backend-rs/Cargo.toml
|
||||
|
@ -15,33 +20,36 @@ 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/
|
||||
|
||||
# Install cargo dependencies
|
||||
RUN cargo fetch --locked --manifest-path /firefish/packages/backend-rs/Cargo.toml
|
||||
# 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
|
||||
|
|
7
Makefile
7
Makefile
|
@ -3,7 +3,7 @@ export
|
|||
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: format entities napi-index
|
||||
pre-commit: format entities napi
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
|
@ -11,11 +11,12 @@ format:
|
|||
|
||||
.PHONY: entities
|
||||
entities:
|
||||
pnpm --filter=backend run build:debug
|
||||
pnpm run migrate
|
||||
$(MAKE) -C ./packages/backend-rs regenerate-entities
|
||||
|
||||
.PHONY: napi-index
|
||||
napi-index:
|
||||
.PHONY: napi
|
||||
napi:
|
||||
$(MAKE) -C ./packages/backend-rs update-index
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
|
@ -14,14 +14,15 @@
|
|||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.vue"],
|
||||
"include": ["*.vue", "packages/client/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useImportType": "warn",
|
||||
"useShorthandFunctionType": "warn",
|
||||
"useTemplate": "warn",
|
||||
"noNonNullAssertion": "off"
|
||||
"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
|
||||
|
|
|
@ -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,27 @@
|
|||
|
||||
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.
|
||||
|
|
|
@ -5,6 +5,36 @@ 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
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'UserprofileJsonbToArray1714270605574',
|
||||
'DropUnusedUserprofileColumns1714259023878',
|
||||
'AntennaJsonbToArray1714192520471',
|
||||
'AddUserProfileLanguage1714888400293',
|
||||
'DropUnusedIndexes1714643926317',
|
||||
'AlterAkaType1714099399879',
|
||||
'AddDriveFileUsage1713451569342',
|
||||
'ConvertCwVarcharToText1713225866247',
|
||||
'FixChatFileConstraint1712855579316',
|
||||
'DropTimeZone1712425488543',
|
||||
'ExpandNoteEdit1711936358554',
|
||||
|
@ -22,6 +30,77 @@ 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;
|
||||
|
@ -728,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";
|
||||
|
|
|
@ -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,39 @@
|
|||
|
||||
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
|
||||
|
|
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"),
|
||||
);
|
||||
});
|
|
@ -19,7 +19,7 @@ deleteAndEditConfirm: Сигурни ли сте, че искате да изт
|
|||
copyUsername: Копиране на потребителското име
|
||||
searchUser: Търсене на потребител
|
||||
reply: Отговор
|
||||
showMore: Покажи още
|
||||
showMore: Показване на повече
|
||||
loadMore: Зареди още
|
||||
followRequestAccepted: Заявката за последване е приета
|
||||
importAndExport: Импорт/експорт на данни
|
||||
|
@ -69,7 +69,7 @@ renameFile: Преименуване на файла
|
|||
_widgets:
|
||||
activity: Дейност
|
||||
notifications: Известия
|
||||
timeline: Инфопоток
|
||||
timeline: Хронология
|
||||
clock: Часовник
|
||||
trends: Актуални
|
||||
photos: Снимки
|
||||
|
@ -187,7 +187,7 @@ notesAndReplies: Публикации и отговори
|
|||
noSuchUser: Потребителят не е намерен
|
||||
pinnedPages: Закачени страници
|
||||
pinLimitExceeded: Не може да закачаш повече публикации
|
||||
flagShowTimelineReplies: Показване на отговори в инфопотока
|
||||
flagShowTimelineReplies: Показване на отговори в хронологията
|
||||
followersCount: Брой последователи
|
||||
receivedReactionsCount: Брой получени реакции
|
||||
federation: Федерация
|
||||
|
@ -336,11 +336,15 @@ _pages:
|
|||
title: Заглавие
|
||||
my: Моите страници
|
||||
pageSetting: Настройки на страницата
|
||||
url: Адрес на страницата
|
||||
summary: Кратко обобщение
|
||||
alignCenter: Центриране на елементите
|
||||
variables: Променливи
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: Известия
|
||||
mentions: Споменавания
|
||||
tl: Инфопоток
|
||||
tl: Хронология
|
||||
direct: Директни съобщения
|
||||
list: Списък
|
||||
antenna: Антена
|
||||
|
@ -375,7 +379,7 @@ basicSettings: Основни настройки
|
|||
otherSettings: Други настройки
|
||||
openInWindow: Отваряне в прозорец
|
||||
profile: Профил
|
||||
timeline: Инфопоток
|
||||
timeline: Хронология
|
||||
noAccountDescription: Този потребител все още не е написал своята биография.
|
||||
login: Вход
|
||||
loggingIn: Вписване
|
||||
|
@ -398,7 +402,7 @@ sendMessage: Изпращане на съобщение
|
|||
jumpToPrevious: Премини към предишно
|
||||
newer: по-ново
|
||||
older: по-старо
|
||||
showLess: Покажи по-малко
|
||||
showLess: Показване на по-малко
|
||||
youGotNewFollower: те последва
|
||||
receiveFollowRequest: Заявка за последване получена
|
||||
mention: Споменаване
|
||||
|
@ -558,12 +562,12 @@ _visibility:
|
|||
specified: Директна
|
||||
localOnly: Само местни
|
||||
public: Общодостъпна
|
||||
publicDescription: Публикацията ще бъде видима във всички публични инфопотоци
|
||||
publicDescription: Публикацията ще бъде видима във всички публични хронологии
|
||||
home: Скрита
|
||||
localOnlyDescription: Не е видима за отдалечени потребители
|
||||
specifiedDescription: Видима само за определени потребители
|
||||
followersDescription: Видима само за последователите ти и споменатите потребители
|
||||
homeDescription: Публикуване само в началния инфопоток
|
||||
homeDescription: Публикуване само в началната хронология
|
||||
explore: Разглеждане
|
||||
theme: Теми
|
||||
wallpaper: Тапет
|
||||
|
@ -594,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.
|
||||
|
@ -754,7 +758,7 @@ _feeds:
|
|||
general: Общи
|
||||
metadata: Метаданни
|
||||
disk: Диск
|
||||
featured: Представени
|
||||
featured: Препоръчано
|
||||
yearsOld: на {age} години
|
||||
reload: Опресняване
|
||||
invites: Покани
|
||||
|
@ -778,8 +782,8 @@ uploadFromUrl: Качване от URL адрес
|
|||
instanceName: Име на сървъра
|
||||
instanceDescription: Описание на сървъра
|
||||
accept: Приемане
|
||||
enableLocalTimeline: Включване на местния инфопоток
|
||||
enableGlobalTimeline: Включване на глобалния инфопоток
|
||||
enableLocalTimeline: Включване на местната хронология
|
||||
enableGlobalTimeline: Включване на глобалната хронология
|
||||
removeMember: Премахване на член
|
||||
isAdmin: Администратор
|
||||
isModerator: Модератор
|
||||
|
@ -862,8 +866,8 @@ apply: Прилагане
|
|||
selectAccount: Избор на акаунт
|
||||
muteThread: Заглушаване на нишката
|
||||
ffVisibility: Видимост на Последвани/Последователи
|
||||
renoteMute: Заглушаване на подсилванията в инфопотоците
|
||||
replyMute: Заглушаване на отговорите в инфопотоците
|
||||
renoteMute: Заглуш. на подсилванията в хронолог.
|
||||
replyMute: Заглуш. на отговорите в хронолог.
|
||||
blockConfirm: Сигурни ли сте, че искате да блокирате този акаунт?
|
||||
appearance: Облик
|
||||
fontSize: Размер на шрифта
|
||||
|
@ -893,7 +897,7 @@ charts: Диаграми
|
|||
disablePagesScript: Изключване на AiScript в Страниците
|
||||
updatedAt: Обновено на
|
||||
privateDescription: Видима само за теб
|
||||
enableTimelineStreaming: Автоматично обновяване на инфопотоците
|
||||
enableTimelineStreaming: Автоматично обновяване на хронологиите
|
||||
toEdit: Редактиране
|
||||
showEmojisInReactionNotifications: Показване на емоджита в известията за реакции
|
||||
rememberNoteVisibility: Запомняне на настройките за видимост на публикациите
|
||||
|
@ -932,3 +936,19 @@ clientSettings: Настройки за устройството
|
|||
behavior: Поведение
|
||||
detectPostLanguage: Автоматично откриване на езика и показване на бутон за превеждане
|
||||
за публикации на чужди езици
|
||||
replyUnmute: Отмяна на заглушаването на отговорите
|
||||
searchWords: Думи за търсене / ID или URL за поглеждане
|
||||
reloadConfirm: Искате ли да опресните хронологията?
|
||||
enableRecommendedTimeline: Включване на препоръчаната хронология
|
||||
showGapBetweenNotesInTimeline: Показване на празнина между публикациите в хронологията
|
||||
lookup: Поглеждане
|
||||
media: Мултимедия
|
||||
welcomeBackWithName: Добре дошли отново, {name}
|
||||
reduceUiAnimation: Намаляване на UI анимациите
|
||||
clickToFinishEmailVerification: Моля, натиснете [{ok}], за да завършите потвърждаването
|
||||
на ел. поща.
|
||||
_cw:
|
||||
show: Показване на съдържанието
|
||||
remoteFollow: Отдалечено последване
|
||||
messagingUnencryptedInfo: Чатовете във Firefish не са шифровани от край до край. Не
|
||||
споделяйте чувствителна информация през Firefish.
|
||||
|
|
|
@ -738,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"
|
||||
|
@ -2289,3 +2290,20 @@ autocorrectNoteLanguage: Mostra un avís si l'idioma de la publicació no coinci
|
|||
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ó
|
||||
|
|
|
@ -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"
|
||||
|
@ -764,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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -2143,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"
|
||||
|
@ -2229,3 +2242,7 @@ autocorrectNoteLanguage: "Show a warning if the post language does not match the
|
|||
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"
|
|
@ -928,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"
|
||||
|
@ -1140,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).
|
||||
|
@ -1258,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."
|
||||
|
@ -1834,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"
|
||||
|
@ -2318,3 +2321,20 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util
|
|||
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
|
||||
|
|
|
@ -1825,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"
|
||||
|
@ -2267,3 +2268,14 @@ markLocalFilesNsfwByDefaultDescription: Terlepas dari pengaturan ini, pengguna d
|
|||
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
|
||||
|
|
|
@ -685,6 +685,9 @@ unclip: "クリップ解除"
|
|||
confirmToUnclipAlreadyClippedNote: "この投稿はすでにクリップ「{name}」に含まれています。投稿をこのクリップから除外しますか?"
|
||||
public: "公開"
|
||||
i18nInfo: "Firefishは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
||||
i18nServerInfo: "新しい端末では{language}が既定の言語になります。"
|
||||
i18nServerChange: "{language}に変更する。"
|
||||
i18nServerSet: "新しい端末での表示言語を{language}にします。"
|
||||
manageAccessTokens: "アクセストークンの管理"
|
||||
accountInfo: "アカウント情報"
|
||||
notesCount: "投稿の数"
|
||||
|
@ -1902,6 +1905,7 @@ _notification:
|
|||
reacted: がリアクションしました
|
||||
renoted: がブーストしました
|
||||
voted: が投票しました
|
||||
andCountUsers: と{count}人{acted}
|
||||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
|
@ -2057,3 +2061,15 @@ incorrectLanguageWarning: "この投稿は{detected}で書かれていると判
|
|||
markLocalFilesNsfwByDefault: このサーバーの全てのファイルをデフォルトでNSFWに設定する
|
||||
markLocalFilesNsfwByDefaultDescription: この設定が有効でも、ユーザーは自分でNSFWのフラグを外すことができます。また、この設定は既存のファイルには影響しません。
|
||||
noteEditHistory: 編集履歴
|
||||
showAddFileDescriptionAtFirstPost: 説明の無い添付ファイルを投稿しようとした際に説明を書く画面を自動で開く
|
||||
antennaLimit: 各ユーザーが作れるアンテナの最大数
|
||||
inputAccountId: 'あなたのアカウントを入力してください(例: @firefish@info.firefish.dev)'
|
||||
remoteFollow: リモートフォロー
|
||||
cannotEditVisibility: 公開範囲は変更できません
|
||||
useThisAccountConfirm: このアカウントで操作を続けますか?
|
||||
getQrCode: QRコードを表示
|
||||
copyRemoteFollowUrl: リモートからフォローするURLをコピー
|
||||
foldNotification: 同じ種類の通知をまとめて表示する
|
||||
slashQuote: 繋げて引用
|
||||
mergeRenotesInTimeline: タイムラインで同じ投稿のブーストをまとめる
|
||||
mergeThreadInTimeline: タイムラインで同じスレッドの投稿をまとめる
|
||||
|
|
|
@ -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: "帖子数量"
|
||||
|
@ -877,6 +882,8 @@ driveCapOverrideCaption: "输入 0 或以下的值将容量重置为默认值。
|
|||
requireAdminForView: "您需要使用管理员账号登录才能查看。"
|
||||
isSystemAccount: "该账号由系统自动创建。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。"
|
||||
typeToConfirm: "输入 {x} 以确认操作"
|
||||
useThisAccountConfirm: "您想使用此帐户继续执行此操作吗?"
|
||||
inputAccountId: "请输入您的帐户(例如 @firefish@info.firefish.dev )"
|
||||
deleteAccount: "删除账号"
|
||||
document: "文档"
|
||||
numberOfPageCache: "缓存页数"
|
||||
|
@ -1385,7 +1392,7 @@ _poll:
|
|||
_visibility:
|
||||
public: "公开"
|
||||
publicDescription: "您的帖子将出现在公共时间线上"
|
||||
home: "不公开"
|
||||
home: "悄悄公开"
|
||||
homeDescription: "仅发送至首页时间线"
|
||||
followers: "仅关注者"
|
||||
followersDescription: "仅对您的关注者和提及的用户可见"
|
||||
|
@ -1786,6 +1793,7 @@ _notification:
|
|||
reacted: 回应了您的帖子
|
||||
voted: 在您的问卷调查中投了票
|
||||
renoted: 转发了您的帖子
|
||||
andCountUsers: "和其他 {count} 名用户{acted}"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "总是显示主列"
|
||||
columnAlign: "列对齐"
|
||||
|
@ -1971,6 +1979,9 @@ origin: 起源
|
|||
confirm: 确认
|
||||
importZip: 导入 ZIP
|
||||
exportZip: 导出 ZIP
|
||||
getQrCode: "获取二维码"
|
||||
remoteFollow: "远程关注"
|
||||
copyRemoteFollowUrl: "复制远程关注 URL"
|
||||
emojiPackCreator: 表情包创建工具
|
||||
objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
|
||||
的端点 URL。
|
||||
|
@ -2053,7 +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: "貼文數量"
|
||||
|
|
38
package.json
38
package.json
|
@ -1,22 +1,23 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240413",
|
||||
"version": "20240516",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.6",
|
||||
"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",
|
||||
|
@ -24,9 +25,10 @@
|
|||
"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",
|
||||
"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 --",
|
||||
|
@ -36,21 +38,17 @@
|
|||
"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.4",
|
||||
"@biomejs/cli-darwin-arm64": "^1.6.4",
|
||||
"@biomejs/cli-darwin-x64": "^1.6.4",
|
||||
"@biomejs/cli-linux-arm64": "^1.6.4",
|
||||
"@biomejs/cli-linux-x64": "^1.6.4",
|
||||
"@types/node": "20.12.7",
|
||||
"execa": "8.0.1",
|
||||
"pnpm": "8.15.6",
|
||||
"@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,6 +4,7 @@ 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
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
.github
|
||||
npm
|
||||
.eslintrc
|
||||
rustfmt.toml
|
||||
yarn.lock
|
||||
*.node
|
||||
.yarn
|
||||
__test__
|
||||
renovate.json
|
|
@ -7,6 +7,7 @@ rust-version = "1.74"
|
|||
[features]
|
||||
default = []
|
||||
napi = ["dep:napi", "dep:napi-derive"]
|
||||
ci = []
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
@ -17,29 +18,41 @@ macro_rs = { workspace = true }
|
|||
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 }
|
||||
cfg-if = { workspace = true }
|
||||
bb8 = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cuid2 = { workspace = true }
|
||||
emojis = { workspace = true }
|
||||
idna = { workspace = true }
|
||||
jsonschema = { workspace = true }
|
||||
image = { workspace = true }
|
||||
isahc = { workspace = true }
|
||||
nom-exif = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parse-display = { workspace = true }
|
||||
openssl = { workspace = true, features = ["vendored"] }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true, default-features = false, features = ["streams", "tokio-comp"] }
|
||||
regex = { workspace = true }
|
||||
schemars = { workspace = true, features = ["chrono"] }
|
||||
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 = { workspace = true }
|
||||
tokio-test = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
|
|
|
@ -6,6 +6,7 @@ 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)' \
|
||||
|
@ -16,8 +17,9 @@ regenerate-entities:
|
|||
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)]\n#[cfg_attr(not(feature = "napi"), derive(Clone))]\n#[cfg_attr(feature = "napi", napi_derive::napi)]/' \
|
||||
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 --
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
520
packages/backend-rs/index.d.ts
vendored
520
packages/backend-rs/index.d.ts
vendored
|
@ -3,6 +3,31 @@
|
|||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export const SECOND: number
|
||||
export const MINUTE: number
|
||||
export const HOUR: number
|
||||
export const DAY: number
|
||||
export const USER_ONLINE_THRESHOLD: number
|
||||
export const USER_ACTIVE_THRESHOLD: number
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const FILE_TYPE_BROWSERSAFE: string[]
|
||||
export interface EnvConfig {
|
||||
onlyQueue: boolean
|
||||
onlyServer: boolean
|
||||
noDaemons: boolean
|
||||
disableClustering: boolean
|
||||
verbose: boolean
|
||||
withLogTime: boolean
|
||||
slow: boolean
|
||||
}
|
||||
export function loadEnv(): EnvConfig
|
||||
export interface ServerConfig {
|
||||
url: string
|
||||
port: number
|
||||
|
@ -16,10 +41,9 @@ export interface ServerConfig {
|
|||
proxySmtp?: string
|
||||
proxyBypassHosts?: Array<string>
|
||||
allowedPrivateNetworks?: Array<string>
|
||||
/** `NapiValue` is not implemented for `u64` */
|
||||
maxFileSize?: number
|
||||
accessLog?: string
|
||||
clusterLimits?: WorkerConfig
|
||||
clusterLimits?: WorkerConfigInternal
|
||||
cuid?: IdConfig
|
||||
outgoingAddress?: string
|
||||
deliverJobConcurrency?: number
|
||||
|
@ -28,7 +52,9 @@ export interface ServerConfig {
|
|||
inboxJobPerSec?: number
|
||||
deliverJobMaxAttempts?: number
|
||||
inboxJobMaxAttempts?: number
|
||||
/** deprecated */
|
||||
logLevel?: Array<string>
|
||||
maxLogLevel?: string
|
||||
syslog?: SysLogConfig
|
||||
proxyRemoteFiles?: boolean
|
||||
mediaProxy?: string
|
||||
|
@ -60,13 +86,17 @@ export interface RedisConfig {
|
|||
pass?: string
|
||||
tls?: TlsConfig
|
||||
db: number
|
||||
prefix: string
|
||||
prefix?: string
|
||||
}
|
||||
export interface TlsConfig {
|
||||
host: string
|
||||
rejectUnauthorized: boolean
|
||||
}
|
||||
export interface WorkerConfig {
|
||||
web: number
|
||||
queue: number
|
||||
}
|
||||
export interface WorkerConfigInternal {
|
||||
web?: number
|
||||
queue?: number
|
||||
}
|
||||
|
@ -111,30 +141,186 @@ export interface ObjectStorageConfig {
|
|||
setPublicReadOnUpload?: boolean
|
||||
s3ForcePathStyle?: boolean
|
||||
}
|
||||
export function readServerConfig(): ServerConfig
|
||||
export interface Config {
|
||||
url: string
|
||||
port: number
|
||||
bind?: string
|
||||
disableHsts?: boolean
|
||||
db: DbConfig
|
||||
redis: RedisConfig
|
||||
cacheServer?: RedisConfig
|
||||
proxy?: string
|
||||
proxySmtp?: string
|
||||
proxyBypassHosts?: Array<string>
|
||||
allowedPrivateNetworks?: Array<string>
|
||||
maxFileSize?: number
|
||||
accessLog?: string
|
||||
clusterLimits: WorkerConfig
|
||||
cuid?: IdConfig
|
||||
outgoingAddress?: string
|
||||
deliverJobConcurrency?: number
|
||||
inboxJobConcurrency?: number
|
||||
deliverJobPerSec?: number
|
||||
inboxJobPerSec?: number
|
||||
deliverJobMaxAttempts?: number
|
||||
inboxJobMaxAttempts?: number
|
||||
/** deprecated */
|
||||
logLevel?: Array<string>
|
||||
maxLogLevel?: string
|
||||
syslog?: SysLogConfig
|
||||
proxyRemoteFiles?: boolean
|
||||
mediaProxy?: string
|
||||
summalyProxyUrl?: string
|
||||
reservedUsernames?: Array<string>
|
||||
maxUserSignups?: number
|
||||
isManagedHosting?: boolean
|
||||
maxNoteLength: number
|
||||
maxCaptionLength: number
|
||||
deepl?: DeepLConfig
|
||||
libreTranslate?: LibreTranslateConfig
|
||||
email?: EmailConfig
|
||||
objectStorage?: ObjectStorageConfig
|
||||
version: string
|
||||
host: string
|
||||
hostname: string
|
||||
redisKeyPrefix: string
|
||||
scheme: string
|
||||
wsScheme: string
|
||||
apiUrl: string
|
||||
wsUrl: string
|
||||
authUrl: string
|
||||
driveUrl: string
|
||||
userAgent: string
|
||||
clientEntry: Manifest
|
||||
}
|
||||
export interface Manifest {
|
||||
file: string
|
||||
name: string
|
||||
src: string
|
||||
isEntry: boolean
|
||||
isDynamicEntry: boolean
|
||||
imports: Array<string>
|
||||
dynamicImports: Array<string>
|
||||
css: Array<string>
|
||||
assets: Array<string>
|
||||
}
|
||||
export function loadConfig(): Config
|
||||
export interface Acct {
|
||||
username: string
|
||||
host: string | null
|
||||
}
|
||||
export function stringToAcct(acct: string): Acct
|
||||
export function acctToString(acct: Acct): string
|
||||
export interface NoteLike {
|
||||
fileIds: Array<string>
|
||||
userId: string | null
|
||||
text: string | null
|
||||
cw: string | null
|
||||
renoteId: string | null
|
||||
replyId: string | null
|
||||
}
|
||||
export function checkWordMute(note: NoteLike, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean>
|
||||
export function initializeRustLogger(): void
|
||||
export function showServerInfo(): void
|
||||
/**
|
||||
* Checks if a server is blocked.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isBlockedServer(host: string): Promise<boolean>
|
||||
/**
|
||||
* Checks if a server is silenced.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isSilencedServer(host: string): Promise<boolean>
|
||||
/**
|
||||
* Checks if a server is allowlisted.
|
||||
* Returns `Ok(true)` if private mode is disabled.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isAllowedServer(host: string): Promise<boolean>
|
||||
export function checkWordMute(note: NoteLike, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean>
|
||||
export function getFullApAccount(username: string, host?: string | undefined | null): string
|
||||
export function isSelfHost(host?: string | undefined | null): boolean
|
||||
export function isSameOrigin(uri: string): boolean
|
||||
export function extractHost(uri: string): string
|
||||
export function toPuny(host: string): string
|
||||
export function isUnicodeEmoji(s: string): boolean
|
||||
export function sqlLikeEscape(src: string): string
|
||||
export function safeForSql(src: string): boolean
|
||||
/** Convert milliseconds to a human readable string */
|
||||
export function formatMilliseconds(milliseconds: number): string
|
||||
export interface ImageSize {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
export function getImageSizeFromUrl(url: string): Promise<ImageSize>
|
||||
/** TODO: handle name collisions better */
|
||||
export interface NoteLikeForAllTexts {
|
||||
fileIds: Array<string>
|
||||
userId: string
|
||||
text: string | null
|
||||
cw: string | null
|
||||
renoteId: string | null
|
||||
replyId: string | null
|
||||
}
|
||||
export interface NoteLikeForGetNoteSummary {
|
||||
fileIds: Array<string>
|
||||
text: string | null
|
||||
cw: string | null
|
||||
hasPoll: boolean
|
||||
}
|
||||
export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
|
||||
export function isQuote(note: Note): boolean
|
||||
export function isSafeUrl(url: string): boolean
|
||||
export function latestVersion(): Promise<string>
|
||||
export function toMastodonId(firefishId: string): string | null
|
||||
export function fromMastodonId(mastodonId: string): string | null
|
||||
export function fetchMeta(useCache: boolean): Promise<Meta>
|
||||
export interface PugArgs {
|
||||
img: string | null
|
||||
title: string
|
||||
instanceName: string
|
||||
desc: string | null
|
||||
icon: string | null
|
||||
splashIcon: string | null
|
||||
themeColor: string | null
|
||||
randomMotd: string
|
||||
privateMode: boolean | null
|
||||
}
|
||||
export function metaToPugArgs(meta: Meta): PugArgs
|
||||
export function nyaify(text: string, lang?: string | undefined | null): string
|
||||
export function hashPassword(password: string): string
|
||||
export function verifyPassword(password: string, hash: string): boolean
|
||||
export function isOldPasswordAlgorithm(hash: string): boolean
|
||||
export interface DecodedReaction {
|
||||
reaction: string
|
||||
name: string | null
|
||||
host: string | null
|
||||
}
|
||||
export function decodeReaction(reaction: string): DecodedReaction
|
||||
export function countReactions(reactions: Record<string, number>): Record<string, number>
|
||||
export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
|
||||
/** Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago */
|
||||
export function removeOldAttestationChallenges(): Promise<void>
|
||||
export interface Cpu {
|
||||
model: string
|
||||
cores: number
|
||||
}
|
||||
export interface Memory {
|
||||
/** Total memory amount in bytes */
|
||||
total: number
|
||||
/** Used memory amount in bytes */
|
||||
used: number
|
||||
/** Available (for (re)use) memory amount in bytes */
|
||||
available: number
|
||||
}
|
||||
export interface Storage {
|
||||
/** Total storage space in bytes */
|
||||
total: number
|
||||
/** Used storage space in bytes */
|
||||
used: number
|
||||
}
|
||||
export function cpuInfo(): Cpu
|
||||
export function cpuUsage(): number
|
||||
export function memoryUsage(): Memory
|
||||
export function storageUsage(): Storage | null
|
||||
export interface AbuseUserReport {
|
||||
id: string
|
||||
createdAt: Date
|
||||
|
@ -196,7 +382,6 @@ export interface Antenna {
|
|||
name: string
|
||||
src: AntennaSrcEnum
|
||||
userListId: string | null
|
||||
keywords: Json
|
||||
withFile: boolean
|
||||
expression: string | null
|
||||
notify: boolean
|
||||
|
@ -204,8 +389,9 @@ export interface Antenna {
|
|||
withReplies: boolean
|
||||
userGroupJoiningId: string | null
|
||||
users: Array<string>
|
||||
excludeKeywords: Json
|
||||
instances: Json
|
||||
instances: Array<string>
|
||||
keywords: Array<string>
|
||||
excludeKeywords: Array<string>
|
||||
}
|
||||
export interface App {
|
||||
id: string
|
||||
|
@ -300,6 +486,7 @@ export interface DriveFile {
|
|||
webpublicType: string | null
|
||||
requestHeaders: Json | null
|
||||
requestIp: string | null
|
||||
usageHint: DriveFileUsageHintEnum | null
|
||||
}
|
||||
export interface DriveFolder {
|
||||
id: string
|
||||
|
@ -505,6 +692,7 @@ export interface Meta {
|
|||
donationLink: string | null
|
||||
moreUrls: Json
|
||||
markLocalFilesNsfwByDefault: boolean
|
||||
antennaLimit: number
|
||||
}
|
||||
export interface Migrations {
|
||||
id: number
|
||||
|
@ -724,81 +912,85 @@ export interface ReplyMuting {
|
|||
muteeId: string
|
||||
muterId: string
|
||||
}
|
||||
export const enum AntennaSrcEnum {
|
||||
All = 0,
|
||||
Group = 1,
|
||||
Home = 2,
|
||||
Instances = 3,
|
||||
List = 4,
|
||||
Users = 5
|
||||
export enum AntennaSrcEnum {
|
||||
All = 'all',
|
||||
Group = 'group',
|
||||
Home = 'home',
|
||||
Instances = 'instances',
|
||||
List = 'list',
|
||||
Users = 'users'
|
||||
}
|
||||
export const enum MutedNoteReasonEnum {
|
||||
Manual = 0,
|
||||
Other = 1,
|
||||
Spam = 2,
|
||||
Word = 3
|
||||
export enum DriveFileUsageHintEnum {
|
||||
UserAvatar = 'userAvatar',
|
||||
UserBanner = 'userBanner'
|
||||
}
|
||||
export const enum NoteVisibilityEnum {
|
||||
Followers = 0,
|
||||
Hidden = 1,
|
||||
Home = 2,
|
||||
Public = 3,
|
||||
Specified = 4
|
||||
export enum MutedNoteReasonEnum {
|
||||
Manual = 'manual',
|
||||
Other = 'other',
|
||||
Spam = 'spam',
|
||||
Word = 'word'
|
||||
}
|
||||
export const enum NotificationTypeEnum {
|
||||
App = 0,
|
||||
Follow = 1,
|
||||
FollowRequestAccepted = 2,
|
||||
GroupInvited = 3,
|
||||
Mention = 4,
|
||||
PollEnded = 5,
|
||||
PollVote = 6,
|
||||
Quote = 7,
|
||||
Reaction = 8,
|
||||
ReceiveFollowRequest = 9,
|
||||
Renote = 10,
|
||||
Reply = 11
|
||||
export enum NoteVisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Hidden = 'hidden',
|
||||
Home = 'home',
|
||||
Public = 'public',
|
||||
Specified = 'specified'
|
||||
}
|
||||
export const enum PageVisibilityEnum {
|
||||
Followers = 0,
|
||||
Public = 1,
|
||||
Specified = 2
|
||||
export enum NotificationTypeEnum {
|
||||
App = 'app',
|
||||
Follow = 'follow',
|
||||
FollowRequestAccepted = 'followRequestAccepted',
|
||||
GroupInvited = 'groupInvited',
|
||||
Mention = 'mention',
|
||||
PollEnded = 'pollEnded',
|
||||
PollVote = 'pollVote',
|
||||
Quote = 'quote',
|
||||
Reaction = 'reaction',
|
||||
ReceiveFollowRequest = 'receiveFollowRequest',
|
||||
Renote = 'renote',
|
||||
Reply = 'reply'
|
||||
}
|
||||
export const enum PollNotevisibilityEnum {
|
||||
Followers = 0,
|
||||
Home = 1,
|
||||
Public = 2,
|
||||
Specified = 3
|
||||
export enum PageVisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Public = 'public',
|
||||
Specified = 'specified'
|
||||
}
|
||||
export const enum RelayStatusEnum {
|
||||
Accepted = 0,
|
||||
Rejected = 1,
|
||||
Requesting = 2
|
||||
export enum PollNotevisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Home = 'home',
|
||||
Public = 'public',
|
||||
Specified = 'specified'
|
||||
}
|
||||
export const enum UserEmojimodpermEnum {
|
||||
Add = 0,
|
||||
Full = 1,
|
||||
Mod = 2,
|
||||
Unauthorized = 3
|
||||
export enum RelayStatusEnum {
|
||||
Accepted = 'accepted',
|
||||
Rejected = 'rejected',
|
||||
Requesting = 'requesting'
|
||||
}
|
||||
export const enum UserProfileFfvisibilityEnum {
|
||||
Followers = 0,
|
||||
Private = 1,
|
||||
Public = 2
|
||||
export enum UserEmojimodpermEnum {
|
||||
Add = 'add',
|
||||
Full = 'full',
|
||||
Mod = 'mod',
|
||||
Unauthorized = 'unauthorized'
|
||||
}
|
||||
export const enum UserProfileMutingnotificationtypesEnum {
|
||||
App = 0,
|
||||
Follow = 1,
|
||||
FollowRequestAccepted = 2,
|
||||
GroupInvited = 3,
|
||||
Mention = 4,
|
||||
PollEnded = 5,
|
||||
PollVote = 6,
|
||||
Quote = 7,
|
||||
Reaction = 8,
|
||||
ReceiveFollowRequest = 9,
|
||||
Renote = 10,
|
||||
Reply = 11
|
||||
export enum UserProfileFfvisibilityEnum {
|
||||
Followers = 'followers',
|
||||
Private = 'private',
|
||||
Public = 'public'
|
||||
}
|
||||
export enum UserProfileMutingnotificationtypesEnum {
|
||||
App = 'app',
|
||||
Follow = 'follow',
|
||||
FollowRequestAccepted = 'followRequestAccepted',
|
||||
GroupInvited = 'groupInvited',
|
||||
Mention = 'mention',
|
||||
PollEnded = 'pollEnded',
|
||||
PollVote = 'pollVote',
|
||||
Quote = 'quote',
|
||||
Reaction = 'reaction',
|
||||
ReceiveFollowRequest = 'receiveFollowRequest',
|
||||
Renote = 'renote',
|
||||
Reply = 'reply'
|
||||
}
|
||||
export interface Signin {
|
||||
id: string
|
||||
|
@ -856,10 +1048,10 @@ export interface User {
|
|||
isDeleted: boolean
|
||||
driveCapacityOverrideMb: number | null
|
||||
movedToUri: string | null
|
||||
alsoKnownAs: string | null
|
||||
speakAsCat: boolean
|
||||
emojiModPerm: UserEmojimodpermEnum
|
||||
isIndexable: boolean
|
||||
alsoKnownAs: Array<string> | null
|
||||
}
|
||||
export interface UserGroup {
|
||||
id: string
|
||||
|
@ -937,7 +1129,6 @@ export interface UserProfile {
|
|||
twoFactorSecret: string | null
|
||||
twoFactorEnabled: boolean
|
||||
password: string | null
|
||||
clientData: Json
|
||||
autoAcceptFollowed: boolean
|
||||
alwaysMarkNsfw: boolean
|
||||
carefulBot: boolean
|
||||
|
@ -945,21 +1136,21 @@ export interface UserProfile {
|
|||
securityKeysAvailable: boolean
|
||||
usePasswordLessLogin: boolean
|
||||
pinnedPageId: string | null
|
||||
room: Json
|
||||
injectFeaturedNote: boolean
|
||||
enableWordMute: boolean
|
||||
mutedWords: Json
|
||||
mutingNotificationTypes: Array<UserProfileMutingnotificationtypesEnum>
|
||||
noCrawle: boolean
|
||||
receiveAnnouncementEmail: boolean
|
||||
emailNotificationTypes: Json
|
||||
mutedInstances: Json
|
||||
publicReactions: boolean
|
||||
ffVisibility: UserProfileFfvisibilityEnum
|
||||
moderationNote: string
|
||||
preventAiLearning: boolean
|
||||
isIndexable: boolean
|
||||
mutedPatterns: Array<string>
|
||||
mutedInstances: Array<string>
|
||||
mutedWords: Array<string>
|
||||
lang: string | null
|
||||
}
|
||||
export interface UserPublickey {
|
||||
userId: string
|
||||
|
@ -985,8 +1176,151 @@ export interface Webhook {
|
|||
latestSentAt: Date | null
|
||||
latestStatus: number | null
|
||||
}
|
||||
/** Initializes Cuid2 generator. Must be called before any [create_id]. */
|
||||
export function initIdGenerator(length: number, fingerprint: string): void
|
||||
export function updateAntennasOnNewNote(note: Note, noteAuthor: Acct, noteMutedUsers: Array<string>): Promise<void>
|
||||
export function fetchNodeinfo(host: string): Promise<Nodeinfo>
|
||||
export function nodeinfo_2_1(): Promise<any>
|
||||
export function nodeinfo_2_0(): Promise<any>
|
||||
/** NodeInfo schema version 2.0. https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.0 */
|
||||
export interface Nodeinfo {
|
||||
/** The schema version, must be 2.0. */
|
||||
version: string
|
||||
/** Metadata about server software in use. */
|
||||
software: Software20
|
||||
/** The protocols supported on this server. */
|
||||
protocols: Array<Protocol>
|
||||
/** The third party sites this server can connect to via their application API. */
|
||||
services: Services
|
||||
/** Whether this server allows open self-registration. */
|
||||
openRegistrations: boolean
|
||||
/** Usage statistics for this server. */
|
||||
usage: Usage
|
||||
/** Free form key value pairs for software specific values. Clients should not rely on any specific key present. */
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
/** Metadata about server software in use (version 2.0). */
|
||||
export interface Software20 {
|
||||
/** The canonical name of this server software. */
|
||||
name: string
|
||||
/** The version of this server software. */
|
||||
version: string
|
||||
}
|
||||
export enum Protocol {
|
||||
Activitypub = 'activitypub',
|
||||
Buddycloud = 'buddycloud',
|
||||
Dfrn = 'dfrn',
|
||||
Diaspora = 'diaspora',
|
||||
Libertree = 'libertree',
|
||||
Ostatus = 'ostatus',
|
||||
Pumpio = 'pumpio',
|
||||
Tent = 'tent',
|
||||
Xmpp = 'xmpp',
|
||||
Zot = 'zot'
|
||||
}
|
||||
/** The third party sites this server can connect to via their application API. */
|
||||
export interface Services {
|
||||
/** The third party sites this server can retrieve messages from for combined display with regular traffic. */
|
||||
inbound: Array<Inbound>
|
||||
/** The third party sites this server can publish messages to on the behalf of a user. */
|
||||
outbound: Array<Outbound>
|
||||
}
|
||||
/** The third party sites this server can retrieve messages from for combined display with regular traffic. */
|
||||
export enum Inbound {
|
||||
Atom1 = 'atom1',
|
||||
Gnusocial = 'gnusocial',
|
||||
Imap = 'imap',
|
||||
Pnut = 'pnut',
|
||||
Pop3 = 'pop3',
|
||||
Pumpio = 'pumpio',
|
||||
Rss2 = 'rss2',
|
||||
Twitter = 'twitter'
|
||||
}
|
||||
/** The third party sites this server can publish messages to on the behalf of a user. */
|
||||
export enum Outbound {
|
||||
Atom1 = 'atom1',
|
||||
Blogger = 'blogger',
|
||||
Buddycloud = 'buddycloud',
|
||||
Diaspora = 'diaspora',
|
||||
Dreamwidth = 'dreamwidth',
|
||||
Drupal = 'drupal',
|
||||
Facebook = 'facebook',
|
||||
Friendica = 'friendica',
|
||||
Gnusocial = 'gnusocial',
|
||||
Google = 'google',
|
||||
Insanejournal = 'insanejournal',
|
||||
Libertree = 'libertree',
|
||||
Linkedin = 'linkedin',
|
||||
Livejournal = 'livejournal',
|
||||
Mediagoblin = 'mediagoblin',
|
||||
Myspace = 'myspace',
|
||||
Pinterest = 'pinterest',
|
||||
Pnut = 'pnut',
|
||||
Posterous = 'posterous',
|
||||
Pumpio = 'pumpio',
|
||||
Redmatrix = 'redmatrix',
|
||||
Rss2 = 'rss2',
|
||||
Smtp = 'smtp',
|
||||
Tent = 'tent',
|
||||
Tumblr = 'tumblr',
|
||||
Twitter = 'twitter',
|
||||
Wordpress = 'wordpress',
|
||||
Xmpp = 'xmpp'
|
||||
}
|
||||
/** Usage statistics for this server. */
|
||||
export interface Usage {
|
||||
users: Users
|
||||
localPosts: number | null
|
||||
localComments: number | null
|
||||
}
|
||||
/** statistics about the users of this server. */
|
||||
export interface Users {
|
||||
total: number | null
|
||||
activeHalfyear: number | null
|
||||
activeMonth: number | null
|
||||
}
|
||||
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
|
||||
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
|
||||
export enum PushNotificationKind {
|
||||
Generic = 'generic',
|
||||
Chat = 'chat',
|
||||
ReadAllChats = 'readAllChats',
|
||||
ReadAllChatsInTheRoom = 'readAllChatsInTheRoom',
|
||||
ReadNotifications = 'readNotifications',
|
||||
ReadAllNotifications = 'readAllNotifications'
|
||||
}
|
||||
export function sendPushNotification(receiverUserId: string, kind: PushNotificationKind, content: any): Promise<void>
|
||||
export function publishToChannelStream(channelId: string, userId: string): Promise<void>
|
||||
export enum ChatEvent {
|
||||
Message = 'message',
|
||||
Read = 'read',
|
||||
Deleted = 'deleted',
|
||||
Typing = 'typing'
|
||||
}
|
||||
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): Promise<void>
|
||||
export enum ChatIndexEvent {
|
||||
Message = 'message',
|
||||
Read = 'read'
|
||||
}
|
||||
export function publishToChatIndexStream(userId: string, kind: ChatIndexEvent, object: any): Promise<void>
|
||||
export interface PackedEmoji {
|
||||
id: string
|
||||
aliases: Array<string>
|
||||
name: string
|
||||
category: string | null
|
||||
host: string | null
|
||||
url: string
|
||||
license: string | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
}
|
||||
export function publishToBroadcastStream(emoji: PackedEmoji): Promise<void>
|
||||
export function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): Promise<void>
|
||||
export interface AbuseUserReportLike {
|
||||
id: string
|
||||
targetUserId: string
|
||||
reporterId: string
|
||||
comment: string
|
||||
}
|
||||
export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): Promise<void>
|
||||
export function getTimestamp(id: string): number
|
||||
/**
|
||||
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||
|
@ -996,5 +1330,9 @@ export function getTimestamp(id: string): number
|
|||
*
|
||||
* Ref: https://github.com/paralleldrive/cuid2#parameterized-length
|
||||
*/
|
||||
export function genId(date?: Date | undefined | null): string
|
||||
export function secureRndstr(length?: number | undefined | null): string
|
||||
export function genId(): string
|
||||
/** Generate an ID using a specific datetime */
|
||||
export function genIdAt(date: Date): string
|
||||
/** Generate random string based on [thread_rng] and [Alphanumeric]. */
|
||||
export function generateSecureRandomString(length: number): string
|
||||
export function generateUserToken(): string
|
||||
|
|
|
@ -310,21 +310,57 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, toMastodonId, fromMastodonId, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, 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.readServerConfig = readServerConfig
|
||||
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.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
|
||||
|
@ -334,7 +370,27 @@ module.exports.RelayStatusEnum = RelayStatusEnum
|
|||
module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
|
||||
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
|
||||
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
||||
module.exports.initIdGenerator = initIdGenerator
|
||||
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.secureRndstr = secureRndstr
|
||||
module.exports.genIdAt = genIdAt
|
||||
module.exports.generateSecureRandomString = generateSecureRandomString
|
||||
module.exports.generateUserToken = generateUserToken
|
||||
|
|
|
@ -22,25 +22,14 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.18.1",
|
||||
"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 ./built/",
|
||||
"build:debug": "napi build --features napi --platform ./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:debug && ava",
|
||||
"universal": "napi universal",
|
||||
"version": "napi version",
|
||||
"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(),
|
||||
}
|
||||
}
|
|
@ -1 +1,5 @@
|
|||
pub use server::CONFIG;
|
||||
|
||||
pub mod constant;
|
||||
pub mod environment;
|
||||
pub mod server;
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::fs;
|
|||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct ServerConfig {
|
||||
struct ServerConfig {
|
||||
pub url: String,
|
||||
pub port: u16,
|
||||
/// host to listen on
|
||||
|
@ -22,10 +22,10 @@ pub struct ServerConfig {
|
|||
pub proxy_bypass_hosts: Option<Vec<String>>,
|
||||
|
||||
pub allowed_private_networks: Option<Vec<String>>,
|
||||
/// `NapiValue` is not implemented for `u64`
|
||||
// TODO: i64 -> u64 (NapiValue is not implemented for u64)
|
||||
pub max_file_size: Option<i64>,
|
||||
pub access_log: Option<String>,
|
||||
pub cluster_limits: Option<WorkerConfig>,
|
||||
pub cluster_limits: Option<WorkerConfigInternal>,
|
||||
pub cuid: Option<IdConfig>,
|
||||
pub outgoing_address: Option<String>,
|
||||
|
||||
|
@ -36,8 +36,11 @@ pub struct ServerConfig {
|
|||
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>,
|
||||
|
@ -82,8 +85,7 @@ pub struct RedisConfig {
|
|||
pub tls: Option<TlsConfig>,
|
||||
#[serde(default)]
|
||||
pub db: u32,
|
||||
#[serde(default)]
|
||||
pub prefix: String,
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
|
@ -94,10 +96,16 @@ pub struct TlsConfig {
|
|||
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 WorkerConfig {
|
||||
pub struct WorkerConfigInternal {
|
||||
pub web: Option<u32>,
|
||||
pub queue: Option<u32>,
|
||||
}
|
||||
|
@ -167,17 +175,212 @@ pub struct ObjectStorageConfig {
|
|||
pub s3_force_path_style: Option<bool>,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn read_server_config() -> ServerConfig {
|
||||
#[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,
|
||||
pub client_entry: Manifest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Meta {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
struct ManifestJson {
|
||||
#[serde(rename = "src/init.ts")]
|
||||
pub init_ts: Manifest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
pub struct Manifest {
|
||||
pub file: String,
|
||||
pub name: String,
|
||||
pub src: String,
|
||||
pub is_entry: bool,
|
||||
pub is_dynamic_entry: bool,
|
||||
pub imports: Vec<String>,
|
||||
pub dynamic_imports: Vec<String>,
|
||||
pub css: Vec<String>,
|
||||
pub assets: Vec<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 yaml");
|
||||
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
|
||||
}
|
||||
|
||||
pub static SERVER_CONFIG: Lazy<ServerConfig> = Lazy::new(read_server_config);
|
||||
fn read_meta() -> Meta {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let meta_json = fs::File::open(cwd.join("../../built/meta.json"))
|
||||
.expect("Failed to open 'built/meta.json'");
|
||||
serde_json::from_reader(meta_json).expect("Failed to parse built/meta.json")
|
||||
}
|
||||
|
||||
fn read_manifest() -> Manifest {
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let manifest_json = fs::File::open(cwd.join("../../built/_client_dist_/manifest.json"))
|
||||
.expect("Failed to open 'built/_client_dist_/manifest.json'");
|
||||
let manifest: ManifestJson = serde_json::from_reader(manifest_json)
|
||||
.expect("Failed to parse built/_client_dist_/manifest.json");
|
||||
|
||||
manifest.init_ts
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn load_config() -> Config {
|
||||
let server_config = read_config_file();
|
||||
let version = read_meta().version;
|
||||
let manifest = read_manifest();
|
||||
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,
|
||||
client_entry: manifest,
|
||||
}
|
||||
}
|
||||
|
||||
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,34 +1,8 @@
|
|||
use crate::config::server::SERVER_CONFIG;
|
||||
use sea_orm::{Database, DbConn, DbErr};
|
||||
pub use postgresql::db_conn;
|
||||
pub use redis::key as redis_key;
|
||||
pub use redis::redis_conn;
|
||||
pub use redis::RedisConnError;
|
||||
|
||||
static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new();
|
||||
|
||||
async fn init_database() -> Result<&'static DbConn, DbErr> {
|
||||
let database_uri = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
SERVER_CONFIG.db.user,
|
||||
urlencoding::encode(&SERVER_CONFIG.db.pass),
|
||||
SERVER_CONFIG.db.host,
|
||||
SERVER_CONFIG.db.port,
|
||||
SERVER_CONFIG.db.db,
|
||||
);
|
||||
let conn = Database::connect(database_uri).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_test() {
|
||||
assert!(db_conn().await.is_ok());
|
||||
}
|
||||
}
|
||||
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;
|
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");
|
||||
}
|
2
packages/backend-rs/src/init/mod.rs
Normal file
2
packages/backend-rs/src/init/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
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 use macro_rs::export;
|
||||
pub use macro_rs::{export, ts_only_warn};
|
||||
|
||||
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,74 +0,0 @@
|
|||
#[derive(Debug, PartialEq)]
|
||||
#[crate::export(object)]
|
||||
pub struct Acct {
|
||||
pub username: String,
|
||||
pub host: Option<String>,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn string_to_acct(acct: &str) -> Acct {
|
||||
let split: Vec<&str> = if let Some(stripped) = acct.strip_prefix('@') {
|
||||
stripped
|
||||
} else {
|
||||
acct
|
||||
}
|
||||
.split('@')
|
||||
.collect();
|
||||
|
||||
Acct {
|
||||
username: split[0].to_string(),
|
||||
host: if split.len() == 1 {
|
||||
None
|
||||
} else {
|
||||
Some(split[1].to_string())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn acct_to_string(acct: &Acct) -> String {
|
||||
match &acct.host {
|
||||
Some(host) => format!("{}@{}", acct.username, host),
|
||||
None => acct.username.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{acct_to_string, string_to_acct, Acct};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[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!(acct_to_string(&remote_acct), "firefish@example.com");
|
||||
assert_ne!(acct_to_string(&remote_acct), "mastodon@example.com");
|
||||
assert_eq!(acct_to_string(&local_acct), "MisakaMikoto");
|
||||
assert_ne!(acct_to_string(&local_acct), "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!(string_to_acct("@firefish@example.com"), remote_acct);
|
||||
assert_eq!(string_to_acct("firefish@example.com"), remote_acct);
|
||||
assert_eq!(string_to_acct("@MisakaMikoto"), local_acct);
|
||||
assert_eq!(string_to_acct("MisakaMikoto"), local_acct);
|
||||
}
|
||||
}
|
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)
|
||||
}
|
|
@ -1,79 +1,7 @@
|
|||
use crate::database::db_conn;
|
||||
use crate::model::entity::{drive_file, note};
|
||||
use crate::misc::get_note_all_texts::{all_texts, NoteLike};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sea_orm::{prelude::*, QuerySelect};
|
||||
|
||||
#[crate::export(object)]
|
||||
pub struct NoteLike {
|
||||
pub file_ids: Vec<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub text: Option<String>,
|
||||
pub cw: Option<String>,
|
||||
pub renote_id: Option<String>,
|
||||
pub reply_id: Option<String>,
|
||||
}
|
||||
|
||||
async fn all_texts(note: NoteLike) -> Result<Vec<String>, DbErr> {
|
||||
let db = db_conn().await?;
|
||||
|
||||
let mut texts: Vec<String> = vec![];
|
||||
|
||||
if let Some(text) = note.text {
|
||||
texts.push(text);
|
||||
}
|
||||
if let Some(cw) = note.cw {
|
||||
texts.push(cw);
|
||||
}
|
||||
|
||||
texts.extend(
|
||||
drive_file::Entity::find()
|
||||
.select_only()
|
||||
.column(drive_file::Column::Comment)
|
||||
.filter(drive_file::Column::Id.is_in(note.file_ids))
|
||||
.into_tuple::<Option<String>>()
|
||||
.all(db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
if let Some(renote_id) = note.renote_id {
|
||||
if let Some((text, cw)) = note::Entity::find_by_id(renote_id)
|
||||
.select_only()
|
||||
.columns([note::Column::Text, note::Column::Cw])
|
||||
.into_tuple::<(Option<String>, Option<String>)>()
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
if let Some(t) = text {
|
||||
texts.push(t);
|
||||
}
|
||||
if let Some(c) = cw {
|
||||
texts.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(reply_id) = note.reply_id {
|
||||
if let Some((text, cw)) = note::Entity::find_by_id(reply_id)
|
||||
.select_only()
|
||||
.columns([note::Column::Text, note::Column::Cw])
|
||||
.into_tuple::<(Option<String>, Option<String>)>()
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
if let Some(t) = text {
|
||||
texts.push(t);
|
||||
}
|
||||
if let Some(c) = cw {
|
||||
texts.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(texts)
|
||||
}
|
||||
use sea_orm::DbErr;
|
||||
|
||||
fn convert_regex(js_regex: &str) -> String {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap());
|
||||
|
@ -82,14 +10,13 @@ fn convert_regex(js_regex: &str) -> String {
|
|||
|
||||
fn check_word_mute_impl(
|
||||
texts: &[String],
|
||||
muted_word_lists: &[Vec<String>],
|
||||
muted_words: &[String],
|
||||
muted_patterns: &[String],
|
||||
) -> bool {
|
||||
muted_word_lists.iter().any(|muted_word_list| {
|
||||
muted_words.iter().any(|item| {
|
||||
texts.iter().any(|text| {
|
||||
let text_lower = text.to_lowercase();
|
||||
muted_word_list
|
||||
.iter()
|
||||
item.split_whitespace()
|
||||
.all(|muted_word| text_lower.contains(&muted_word.to_lowercase()))
|
||||
})
|
||||
}) || muted_patterns.iter().any(|muted_pattern| {
|
||||
|
@ -102,16 +29,138 @@ fn check_word_mute_impl(
|
|||
#[crate::export]
|
||||
pub async fn check_word_mute(
|
||||
note: NoteLike,
|
||||
muted_word_lists: Vec<Vec<String>>,
|
||||
muted_patterns: Vec<String>,
|
||||
muted_words: &[String],
|
||||
muted_patterns: &[String],
|
||||
) -> Result<bool, DbErr> {
|
||||
if muted_word_lists.is_empty() && muted_patterns.is_empty() {
|
||||
if muted_words.is_empty() && muted_patterns.is_empty() {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(check_word_mute_impl(
|
||||
&all_texts(note).await?,
|
||||
&muted_word_lists,
|
||||
&muted_patterns,
|
||||
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()]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::config::server::SERVER_CONFIG;
|
||||
use crate::config::CONFIG;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -14,21 +14,21 @@ pub enum Error {
|
|||
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(&SERVER_CONFIG.url)?),
|
||||
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(&SERVER_CONFIG.url)? == to_puny(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() == SERVER_CONFIG.url)
|
||||
Ok(url::Url::parse(uri)?.origin().ascii_serialization() == CONFIG.url)
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
|
|
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%"));
|
||||
}
|
||||
}
|
46
packages/backend-rs/src/misc/format_milliseconds.rs
Normal file
46
packages/backend-rs/src/misc/format_milliseconds.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
/// Convert milliseconds to a human readable string
|
||||
#[crate::export]
|
||||
pub fn format_milliseconds(milliseconds: u32) -> String {
|
||||
let mut seconds = milliseconds / 1000;
|
||||
let mut minutes = seconds / 60;
|
||||
let mut hours = minutes / 60;
|
||||
let days = hours / 24;
|
||||
|
||||
seconds %= 60;
|
||||
minutes %= 60;
|
||||
hours %= 24;
|
||||
|
||||
let mut buf: Vec<String> = vec![];
|
||||
|
||||
if days > 0 {
|
||||
buf.push(format!("{} day(s)", days));
|
||||
}
|
||||
if hours > 0 {
|
||||
buf.push(format!("{} hour(s)", hours));
|
||||
}
|
||||
if minutes > 0 {
|
||||
buf.push(format!("{} minute(s)", minutes));
|
||||
}
|
||||
if seconds > 0 {
|
||||
buf.push(format!("{} second(s)", seconds));
|
||||
}
|
||||
|
||||
buf.join(", ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::format_milliseconds;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn format_milliseconds_test() {
|
||||
assert_eq!(format_milliseconds(1000), "1 second(s)");
|
||||
assert_eq!(format_milliseconds(1387938), "23 minute(s), 7 second(s)");
|
||||
assert_eq!(format_milliseconds(34200457), "9 hour(s), 30 minute(s)");
|
||||
assert_eq!(
|
||||
format_milliseconds(998244353),
|
||||
"11 day(s), 13 hour(s), 17 minute(s), 24 second(s)"
|
||||
);
|
||||
}
|
||||
}
|
219
packages/backend-rs/src/misc/get_image_size.rs
Normal file
219
packages/backend-rs/src/misc/get_image_size.rs
Normal file
|
@ -0,0 +1,219 @@
|
|||
use crate::database::cache;
|
||||
use crate::util::http_client;
|
||||
use image::{io::Reader, ImageError, ImageFormat};
|
||||
use isahc::ReadResponseExt;
|
||||
use nom_exif::{parse_jpeg_exif, EntryValue, ExifTag};
|
||||
use std::io::Cursor;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("HTTP client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
#[error("Isahc error: {0}")]
|
||||
IsahcErr(#[from] isahc::Error),
|
||||
#[error("HTTP error: {0}")]
|
||||
HttpErr(String),
|
||||
#[error("Image decoding error: {0}")]
|
||||
ImageErr(#[from] ImageError),
|
||||
#[error("Image decoding error: {0}")]
|
||||
IoErr(#[from] std::io::Error),
|
||||
#[error("Exif extraction error: {0}")]
|
||||
ExifErr(#[from] nom_exif::Error),
|
||||
#[error("Emoji meta attempt limit exceeded: {0}")]
|
||||
TooManyAttempts(String),
|
||||
#[error("Unsupported image type: {0}")]
|
||||
UnsupportedImageErr(String),
|
||||
}
|
||||
|
||||
const BROWSER_SAFE_IMAGE_TYPES: [ImageFormat; 8] = [
|
||||
ImageFormat::Png,
|
||||
ImageFormat::Jpeg,
|
||||
ImageFormat::Gif,
|
||||
ImageFormat::WebP,
|
||||
ImageFormat::Tiff,
|
||||
ImageFormat::Bmp,
|
||||
ImageFormat::Ico,
|
||||
ImageFormat::Avif,
|
||||
];
|
||||
|
||||
static MTX_GUARD: Mutex<()> = Mutex::const_new(());
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[crate::export(object)]
|
||||
pub struct ImageSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
|
||||
let attempted: bool;
|
||||
|
||||
{
|
||||
let _ = MTX_GUARD.lock().await;
|
||||
|
||||
attempted = cache::get_one::<bool>(cache::Category::FetchUrl, url)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
if !attempted {
|
||||
cache::set_one(cache::Category::FetchUrl, url, &true, 10 * 60).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if attempted {
|
||||
tracing::warn!("attempt limit exceeded: {}", url);
|
||||
return Err(Error::TooManyAttempts(url.to_string()));
|
||||
}
|
||||
|
||||
tracing::info!("retrieving image size from {}", url);
|
||||
|
||||
let mut response = http_client::client()?.get(url)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
tracing::info!("status: {}", response.status());
|
||||
tracing::debug!("response body: {:#?}", response.body());
|
||||
return Err(Error::HttpErr(format!("Failed to get image from {}", url)));
|
||||
}
|
||||
|
||||
let image_bytes = response.bytes()?;
|
||||
|
||||
let reader = Reader::new(Cursor::new(&image_bytes)).with_guessed_format()?;
|
||||
|
||||
let format = reader.format();
|
||||
if format.is_none() || !BROWSER_SAFE_IMAGE_TYPES.contains(&format.unwrap()) {
|
||||
return Err(Error::UnsupportedImageErr(format!("{:?}", format)));
|
||||
}
|
||||
|
||||
let size = reader.into_dimensions()?;
|
||||
|
||||
let res = ImageSize {
|
||||
width: size.0,
|
||||
height: size.1,
|
||||
};
|
||||
|
||||
if format.unwrap() != ImageFormat::Jpeg {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
// handle jpeg orientation
|
||||
// https://magnushoff.com/articles/jpeg-orientation/
|
||||
|
||||
let exif = parse_jpeg_exif(&*image_bytes)?;
|
||||
if exif.is_none() {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let orientation = exif.unwrap().get_value(&ExifTag::Orientation)?;
|
||||
let rotated =
|
||||
orientation.is_some() && matches!(orientation.unwrap(), EntryValue::U32(v) if v >= 5);
|
||||
|
||||
if !rotated {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
Ok(ImageSize {
|
||||
width: size.1,
|
||||
height: size.0,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{get_image_size_from_url, ImageSize};
|
||||
use crate::database::cache;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_image_size() {
|
||||
let png_url_1 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png";
|
||||
let png_url_2 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/notification-badges/at.png";
|
||||
let png_url_3 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/api-doc.png";
|
||||
let rotated_jpeg_url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/test/resources/rotate.jpg";
|
||||
let webp_url_1 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/custom/assets/badges/error.webp";
|
||||
let webp_url_2 = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/screenshots/1.webp";
|
||||
let ico_url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/favicon.ico";
|
||||
let gif_url = "https://firefish.dev/firefish/firefish/-/raw/b9c3dfbd3d473cb2cee20c467eeae780bc401271/packages/backend/test/resources/anime.gif";
|
||||
let mp3_url = "https://firefish.dev/firefish/firefish/-/blob/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/sounds/aisha/1.mp3";
|
||||
|
||||
// delete caches in case you run this test multiple times
|
||||
cache::delete_all(cache::Category::FetchUrl).await.unwrap();
|
||||
|
||||
let png_size_1 = ImageSize {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
};
|
||||
let png_size_2 = ImageSize {
|
||||
width: 96,
|
||||
height: 96,
|
||||
};
|
||||
let png_size_3 = ImageSize {
|
||||
width: 1024,
|
||||
height: 354,
|
||||
};
|
||||
let rotated_jpeg_size = ImageSize {
|
||||
width: 256,
|
||||
height: 512,
|
||||
};
|
||||
let webp_size_1 = ImageSize {
|
||||
width: 256,
|
||||
height: 256,
|
||||
};
|
||||
let webp_size_2 = ImageSize {
|
||||
width: 1080,
|
||||
height: 2340,
|
||||
};
|
||||
let ico_size = ImageSize {
|
||||
width: 256,
|
||||
height: 256,
|
||||
};
|
||||
let gif_size = ImageSize {
|
||||
width: 256,
|
||||
height: 256,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
png_size_1,
|
||||
get_image_size_from_url(png_url_1).await.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
png_size_2,
|
||||
get_image_size_from_url(png_url_2).await.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
png_size_3,
|
||||
get_image_size_from_url(png_url_3).await.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
rotated_jpeg_size,
|
||||
get_image_size_from_url(rotated_jpeg_url).await.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
webp_size_1,
|
||||
get_image_size_from_url(webp_url_1).await.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
webp_size_2,
|
||||
get_image_size_from_url(webp_url_2).await.unwrap()
|
||||
);
|
||||
assert_eq!(ico_size, get_image_size_from_url(ico_url).await.unwrap());
|
||||
assert_eq!(gif_size, get_image_size_from_url(gif_url).await.unwrap());
|
||||
assert!(get_image_size_from_url(mp3_url).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn too_many_attempts() {
|
||||
let url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png";
|
||||
|
||||
// delete caches in case you run this test multiple times
|
||||
cache::delete_one(cache::Category::FetchUrl, url)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(get_image_size_from_url(url).await.is_ok());
|
||||
assert!(get_image_size_from_url(url).await.is_err());
|
||||
}
|
||||
}
|
79
packages/backend-rs/src/misc/get_note_all_texts.rs
Normal file
79
packages/backend-rs/src/misc/get_note_all_texts.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use crate::database::db_conn;
|
||||
use crate::model::entity::{drive_file, note};
|
||||
use sea_orm::{prelude::*, QuerySelect};
|
||||
|
||||
/// TODO: handle name collisions better
|
||||
#[crate::export(object, js_name = "NoteLikeForAllTexts")]
|
||||
pub struct NoteLike {
|
||||
pub file_ids: Vec<String>,
|
||||
pub user_id: String,
|
||||
pub text: Option<String>,
|
||||
pub cw: Option<String>,
|
||||
pub renote_id: Option<String>,
|
||||
pub reply_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn all_texts(note: NoteLike) -> Result<Vec<String>, DbErr> {
|
||||
let db = db_conn().await?;
|
||||
|
||||
let mut texts: Vec<String> = vec![];
|
||||
|
||||
if let Some(text) = note.text {
|
||||
texts.push(text);
|
||||
}
|
||||
if let Some(cw) = note.cw {
|
||||
texts.push(cw);
|
||||
}
|
||||
|
||||
texts.extend(
|
||||
drive_file::Entity::find()
|
||||
.select_only()
|
||||
.column(drive_file::Column::Comment)
|
||||
.filter(drive_file::Column::Id.is_in(note.file_ids))
|
||||
.into_tuple::<Option<String>>()
|
||||
.all(db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
if let Some(renote_id) = ¬e.renote_id {
|
||||
if let Some((text, cw)) = note::Entity::find_by_id(renote_id)
|
||||
.select_only()
|
||||
.columns([note::Column::Text, note::Column::Cw])
|
||||
.into_tuple::<(Option<String>, Option<String>)>()
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
if let Some(t) = text {
|
||||
texts.push(t);
|
||||
}
|
||||
if let Some(c) = cw {
|
||||
texts.push(c);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("nonexistent renote id: {:#?}", renote_id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(reply_id) = ¬e.reply_id {
|
||||
if let Some((text, cw)) = note::Entity::find_by_id(reply_id)
|
||||
.select_only()
|
||||
.columns([note::Column::Text, note::Column::Cw])
|
||||
.into_tuple::<(Option<String>, Option<String>)>()
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
if let Some(t) = text {
|
||||
texts.push(t);
|
||||
}
|
||||
if let Some(c) = cw {
|
||||
texts.push(c);
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("nonexistent reply id: {:#?}", reply_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(texts)
|
||||
}
|
94
packages/backend-rs/src/misc/get_note_summary.rs
Normal file
94
packages/backend-rs/src/misc/get_note_summary.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// TODO: handle name collisions in a better way
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
|
||||
pub struct NoteLike {
|
||||
pub file_ids: Vec<String>,
|
||||
pub text: Option<String>,
|
||||
pub cw: Option<String>,
|
||||
pub has_poll: bool,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn get_note_summary(note: NoteLike) -> String {
|
||||
let mut buf: Vec<String> = vec![];
|
||||
|
||||
if let Some(cw) = note.cw {
|
||||
buf.push(cw)
|
||||
} else if let Some(text) = note.text {
|
||||
buf.push(text)
|
||||
}
|
||||
|
||||
match note.file_ids.len() {
|
||||
0 => (),
|
||||
1 => buf.push("📎".to_string()),
|
||||
n => buf.push(format!("📎 ({})", n)),
|
||||
};
|
||||
|
||||
if note.has_poll {
|
||||
buf.push("📊".to_string())
|
||||
}
|
||||
|
||||
buf.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{get_note_summary, NoteLike};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_note_summary() {
|
||||
let note = NoteLike {
|
||||
file_ids: vec![],
|
||||
text: Some("Hello world!".to_string()),
|
||||
cw: None,
|
||||
has_poll: false,
|
||||
};
|
||||
assert_eq!(get_note_summary(note), "Hello world!");
|
||||
|
||||
let note_with_cw = NoteLike {
|
||||
file_ids: vec![],
|
||||
text: Some("Hello world!".to_string()),
|
||||
cw: Some("Content warning".to_string()),
|
||||
has_poll: false,
|
||||
};
|
||||
assert_eq!(get_note_summary(note_with_cw), "Content warning");
|
||||
|
||||
let note_with_file_and_cw = NoteLike {
|
||||
file_ids: vec!["9s7fmcqogiq4igin".to_string()],
|
||||
text: None,
|
||||
cw: Some("Selfie, no ec".to_string()),
|
||||
has_poll: false,
|
||||
};
|
||||
assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎");
|
||||
|
||||
let note_with_files_only = NoteLike {
|
||||
file_ids: vec![
|
||||
"9s7fmcqogiq4igin".to_string(),
|
||||
"9s7qrld5u14cey98".to_string(),
|
||||
"9s7gebs5zgts4kca".to_string(),
|
||||
"9s5z3e4vefqd29ee".to_string(),
|
||||
],
|
||||
text: None,
|
||||
cw: None,
|
||||
has_poll: false,
|
||||
};
|
||||
assert_eq!(get_note_summary(note_with_files_only), "📎 (4)");
|
||||
|
||||
let note_all = NoteLike {
|
||||
file_ids: vec![
|
||||
"9s7fmcqogiq4igin".to_string(),
|
||||
"9s7qrld5u14cey98".to_string(),
|
||||
"9s7gebs5zgts4kca".to_string(),
|
||||
"9s5z3e4vefqd29ee".to_string(),
|
||||
],
|
||||
text: Some("Hello world!".to_string()),
|
||||
cw: Some("Content warning".to_string()),
|
||||
has_poll: true,
|
||||
};
|
||||
assert_eq!(get_note_summary(note_all), "Content warning 📎 (4) 📊");
|
||||
}
|
||||
}
|
9
packages/backend-rs/src/misc/is_quote.rs
Normal file
9
packages/backend-rs/src/misc/is_quote.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use crate::model::entity::note;
|
||||
|
||||
// https://github.com/napi-rs/napi-rs/issues/2060
|
||||
type Note = note::Model;
|
||||
|
||||
#[crate::export]
|
||||
pub fn is_quote(note: Note) -> bool {
|
||||
note.renote_id.is_some() && (note.text.is_some() || note.has_poll || !note.file_ids.is_empty())
|
||||
}
|
34
packages/backend-rs/src/misc/is_safe_url.rs
Normal file
34
packages/backend-rs/src/misc/is_safe_url.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
#[crate::export]
|
||||
pub fn is_safe_url(url: &str) -> bool {
|
||||
if let Ok(url) = url.parse::<url::Url>() {
|
||||
if url.host_str().unwrap_or_default() == "unix"
|
||||
|| !["http", "https"].contains(&url.scheme())
|
||||
|| ![None, Some(80), Some(443)].contains(&url.port())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::is_safe_url;
|
||||
|
||||
#[test]
|
||||
fn safe_url() {
|
||||
assert!(is_safe_url("http://firefish.dev/firefish/firefish"));
|
||||
assert!(is_safe_url("https://firefish.dev/firefish/firefish"));
|
||||
assert!(is_safe_url("http://firefish.dev:80/firefish/firefish"));
|
||||
assert!(is_safe_url("https://firefish.dev:80/firefish/firefish"));
|
||||
assert!(is_safe_url("http://firefish.dev:443/firefish/firefish"));
|
||||
assert!(is_safe_url("https://firefish.dev:443/firefish/firefish"));
|
||||
assert!(!is_safe_url("https://unix/firefish/firefish"));
|
||||
assert!(!is_safe_url("https://firefish.dev:35/firefish/firefish"));
|
||||
assert!(!is_safe_url("ftp://firefish.dev/firefish/firefish"));
|
||||
assert!(!is_safe_url("nyaa"));
|
||||
assert!(!is_safe_url(""));
|
||||
}
|
||||
}
|
111
packages/backend-rs/src/misc/latest_version.rs
Normal file
111
packages/backend-rs/src/misc/latest_version.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use crate::database::cache;
|
||||
use crate::util::http_client;
|
||||
use isahc::ReadResponseExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("Isahc error: {0}")]
|
||||
IsahcErr(#[from] isahc::Error),
|
||||
#[error("HTTP client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
#[error("HTTP error: {0}")]
|
||||
HttpErr(String),
|
||||
#[error("Response parsing error: {0}")]
|
||||
IoErr(#[from] std::io::Error),
|
||||
#[error("Failed to deserialize JSON: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
const UPSTREAM_PACKAGE_JSON_URL: &str =
|
||||
"https://firefish.dev/firefish/firefish/-/raw/main/package.json";
|
||||
|
||||
async fn get_latest_version() -> Result<String, Error> {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct Response {
|
||||
version: String,
|
||||
}
|
||||
|
||||
let mut response = http_client::client()?.get(UPSTREAM_PACKAGE_JSON_URL)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
tracing::info!("status: {}", response.status());
|
||||
tracing::debug!("response body: {:#?}", response.body());
|
||||
return Err(Error::HttpErr(
|
||||
"Failed to fetch version from Firefish GitLab".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let res_parsed: Response = serde_json::from_str(&response.text()?)?;
|
||||
|
||||
Ok(res_parsed.version)
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn latest_version() -> Result<String, Error> {
|
||||
let version: Option<String> =
|
||||
cache::get_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL).await?;
|
||||
|
||||
if let Some(v) = version {
|
||||
tracing::trace!("use cached value: {}", v);
|
||||
Ok(v)
|
||||
} else {
|
||||
tracing::trace!("cache is expired, fetching the latest version");
|
||||
let fetched_version = get_latest_version().await?;
|
||||
tracing::trace!("fetched value: {}", fetched_version);
|
||||
|
||||
cache::set_one(
|
||||
cache::Category::FetchUrl,
|
||||
UPSTREAM_PACKAGE_JSON_URL,
|
||||
&fetched_version,
|
||||
3 * 60 * 60,
|
||||
)
|
||||
.await?;
|
||||
Ok(fetched_version)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{latest_version, UPSTREAM_PACKAGE_JSON_URL};
|
||||
use crate::database::cache;
|
||||
|
||||
fn validate_version(version: String) {
|
||||
// version: YYYYMMDD or YYYYMMDD-X
|
||||
assert!(version.len() >= 8);
|
||||
assert!(version[..8].chars().all(|c| c.is_ascii_digit()));
|
||||
|
||||
// YYYY
|
||||
assert!(&version[..4] >= "2024");
|
||||
|
||||
// MM
|
||||
assert!(&version[4..6] >= "01");
|
||||
assert!(&version[4..6] <= "12");
|
||||
|
||||
// DD
|
||||
assert!(&version[6..8] >= "01");
|
||||
assert!(&version[6..8] <= "31");
|
||||
|
||||
// -X
|
||||
if version.len() > 8 {
|
||||
assert!(version.chars().nth(8).unwrap() == '-');
|
||||
assert!(version[9..].chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_version() {
|
||||
// delete caches in case you run this test multiple times
|
||||
cache::delete_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// fetch from firefish.dev
|
||||
validate_version(latest_version().await.unwrap());
|
||||
|
||||
// use cache
|
||||
validate_version(latest_version().await.unwrap());
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
#[crate::export]
|
||||
pub fn to_mastodon_id(firefish_id: &str) -> Option<String> {
|
||||
let decoded: [u8; 16] = basen::BASE36.decode_var_len(&firefish_id.to_ascii_lowercase())?;
|
||||
let decoded: [u8; 16] = basen::BASE36.decode_var_len(firefish_id)?;
|
||||
Some(basen::BASE10.encode_var_len(&decoded))
|
||||
}
|
||||
|
||||
|
|
83
packages/backend-rs/src/misc/meta.rs
Normal file
83
packages/backend-rs/src/misc/meta.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use crate::database::db_conn;
|
||||
use crate::model::entity::meta;
|
||||
use rand::prelude::*;
|
||||
use sea_orm::{prelude::*, ActiveValue};
|
||||
use std::sync::Mutex;
|
||||
|
||||
type Meta = meta::Model;
|
||||
|
||||
static CACHE: Mutex<Option<Meta>> = Mutex::new(None);
|
||||
fn update_cache(meta: &Meta) {
|
||||
let _ = CACHE.lock().map(|mut cache| *cache = Some(meta.clone()));
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn fetch_meta(use_cache: bool) -> Result<Meta, DbErr> {
|
||||
// try using cache
|
||||
if use_cache {
|
||||
if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) {
|
||||
return Ok(cache);
|
||||
}
|
||||
}
|
||||
|
||||
// try fetching from db
|
||||
let db = db_conn().await?;
|
||||
let meta = meta::Entity::find().one(db).await?;
|
||||
if let Some(meta) = meta {
|
||||
update_cache(&meta);
|
||||
return Ok(meta);
|
||||
}
|
||||
|
||||
// create a new meta object and insert into db
|
||||
let meta = meta::Entity::insert(meta::ActiveModel {
|
||||
id: ActiveValue::Set("x".to_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec_with_returning(db)
|
||||
.await?;
|
||||
update_cache(&meta);
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
#[crate::export(object)]
|
||||
pub struct PugArgs {
|
||||
pub img: Option<String>,
|
||||
pub title: String,
|
||||
pub instance_name: String,
|
||||
pub desc: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub splash_icon: Option<String>,
|
||||
pub theme_color: Option<String>,
|
||||
pub random_motd: String,
|
||||
pub private_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn meta_to_pug_args(meta: Meta) -> PugArgs {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let splash_icon = meta
|
||||
.custom_splash_icons
|
||||
.choose(&mut rng)
|
||||
.map(|s| s.to_owned())
|
||||
.or_else(|| meta.icon_url.to_owned());
|
||||
|
||||
let random_motd = meta
|
||||
.custom_motd
|
||||
.choose(&mut rng)
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| "Loading...".to_owned());
|
||||
|
||||
let name = meta.name.unwrap_or_else(|| "Firefish".to_owned());
|
||||
PugArgs {
|
||||
img: meta.banner_url,
|
||||
title: name.clone(),
|
||||
instance_name: name.clone(),
|
||||
desc: meta.description,
|
||||
icon: meta.icon_url,
|
||||
splash_icon,
|
||||
theme_color: meta.theme_color,
|
||||
random_motd,
|
||||
private_mode: meta.private_mode,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,20 @@
|
|||
pub mod acct;
|
||||
pub mod check_hit_antenna;
|
||||
pub mod check_server_block;
|
||||
pub mod check_word_mute;
|
||||
pub mod convert_host;
|
||||
pub mod emoji;
|
||||
pub mod escape_sql;
|
||||
pub mod format_milliseconds;
|
||||
pub mod get_image_size;
|
||||
pub mod get_note_all_texts;
|
||||
pub mod get_note_summary;
|
||||
pub mod is_quote;
|
||||
pub mod is_safe_url;
|
||||
pub mod latest_version;
|
||||
pub mod mastodon_id;
|
||||
pub mod meta;
|
||||
pub mod nyaify;
|
||||
pub mod password;
|
||||
pub mod reaction;
|
||||
pub mod remove_old_attestation_challenges;
|
||||
pub mod system_info;
|
||||
|
|
69
packages/backend-rs/src/misc/password.rs
Normal file
69
packages/backend-rs/src/misc/password.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use argon2::{
|
||||
password_hash,
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
#[crate::export]
|
||||
pub fn hash_password(password: &str) -> Result<String, password_hash::errors::Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Ok(Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum VerifyError {
|
||||
#[error("An error occured while bcrypt verification: {0}")]
|
||||
BcryptError(#[from] bcrypt::BcryptError),
|
||||
#[error("Invalid argon2 password hash: {0}")]
|
||||
InvalidArgon2Hash(#[from] password_hash::Error),
|
||||
#[error("An error occured while argon2 verification: {0}")]
|
||||
Argon2Error(#[from] argon2::Error),
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn verify_password(password: &str, hash: &str) -> Result<bool, VerifyError> {
|
||||
if is_old_password_algorithm(hash) {
|
||||
Ok(bcrypt::verify(password, hash)?)
|
||||
} else {
|
||||
let parsed_hash = PasswordHash::new(hash)?;
|
||||
Ok(Argon2::default()
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[crate::export]
|
||||
pub fn is_old_password_algorithm(hash: &str) -> bool {
|
||||
// bcrypt hashes start with $2[ab]$
|
||||
hash.starts_with("$2")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{hash_password, is_old_password_algorithm, verify_password};
|
||||
|
||||
#[test]
|
||||
fn verify_password_test() {
|
||||
let password = "omWc*%sD^fn7o2cXmc9e2QasBdrbRuhNB*gx!J5";
|
||||
|
||||
let hash = hash_password(password).unwrap();
|
||||
assert!(verify_password(password, hash.as_str()).unwrap());
|
||||
|
||||
let argon2_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj5FMgePdGIIq3Jk";
|
||||
let argon2_invalid_hash = "$argon2id$v=19$m=19456,t=2,p=1$jty3puDFd4ENv/lgHn3ROQ$kRHDdEoVv2rruvnF731E74NxnYlvj4FMgePdGIIq3Jk";
|
||||
let bcrypt_hash = "$2a$12$WzUc.20jgbHmQjUMqTr8vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la";
|
||||
let bcrypt_invalid_hash = "$2a$12$WzUc.20jgbHmQjUMqTr7vOhKqYbS1BUvubapv/GLjCK1IN.h4e4la";
|
||||
|
||||
assert!(!is_old_password_algorithm(argon2_hash));
|
||||
assert!(is_old_password_algorithm(bcrypt_hash));
|
||||
|
||||
assert!(verify_password(password, argon2_hash).unwrap());
|
||||
assert!(verify_password(password, bcrypt_hash).unwrap());
|
||||
|
||||
assert!(!verify_password(password, argon2_invalid_hash).unwrap());
|
||||
assert!(!verify_password(password, bcrypt_invalid_hash).unwrap());
|
||||
}
|
||||
}
|
195
packages/backend-rs/src/misc/reaction.rs
Normal file
195
packages/backend-rs/src/misc/reaction.rs
Normal file
|
@ -0,0 +1,195 @@
|
|||
use crate::database::db_conn;
|
||||
use crate::misc::{convert_host::to_puny, emoji::is_unicode_emoji, meta::fetch_meta};
|
||||
use crate::model::entity::emoji;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use sea_orm::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
#[crate::export(object)]
|
||||
pub struct DecodedReaction {
|
||||
pub reaction: String,
|
||||
pub name: Option<String>,
|
||||
pub host: Option<String>,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn decode_reaction(reaction: &str) -> DecodedReaction {
|
||||
// Misskey allows you to include "+" and "-" in emoji shortcodes
|
||||
// MFM spec: https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md?plain=1#L583
|
||||
// Misskey's implementation: https://github.com/misskey-dev/misskey/blob/bba3097765317cbf95d09627961b5b5dce16a972/packages/backend/src/core/ReactionService.ts#L68
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@([0-9A-Za-z_.-]+))?:$").unwrap());
|
||||
|
||||
if let Some(captures) = RE.captures(reaction) {
|
||||
let name = &captures[1];
|
||||
let host = captures.get(2).map(|s| s.as_str());
|
||||
|
||||
DecodedReaction {
|
||||
reaction: format!(":{}@{}:", name, host.unwrap_or(".")),
|
||||
name: Some(name.to_owned()),
|
||||
host: host.map(|s| s.to_owned()),
|
||||
}
|
||||
} else {
|
||||
DecodedReaction {
|
||||
reaction: reaction.to_owned(),
|
||||
name: None,
|
||||
host: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn count_reactions(reactions: &HashMap<String, u32>) -> HashMap<String, u32> {
|
||||
let mut res = HashMap::<String, u32>::new();
|
||||
|
||||
for (reaction, count) in reactions.iter() {
|
||||
if count > &0 {
|
||||
let decoded = decode_reaction(reaction).reaction;
|
||||
let total = res.entry(decoded).or_insert(0);
|
||||
*total += count;
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Idna error: {0}")]
|
||||
IdnaError(#[from] idna::Errors),
|
||||
#[error("Database error: {0}")]
|
||||
DbError(#[from] DbErr),
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Result<String, Error> {
|
||||
if let Some(reaction) = reaction {
|
||||
// FIXME: Is it okay to do this only here?
|
||||
// This was introduced in https://firefish.dev/firefish/firefish/-/commit/af730e75b6fc1a57ca680ce83459d7e433b130cf
|
||||
if reaction.contains('❤') || reaction.contains("♥️") {
|
||||
return Ok("❤️".to_owned());
|
||||
}
|
||||
|
||||
if is_unicode_emoji(reaction) {
|
||||
return Ok(reaction.to_owned());
|
||||
}
|
||||
|
||||
static RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@\.)?:$").unwrap());
|
||||
|
||||
if let Some(captures) = RE.captures(reaction) {
|
||||
let name = &captures[1];
|
||||
let db = db_conn().await?;
|
||||
|
||||
if let Some(host) = host {
|
||||
// remote emoji
|
||||
let ascii_host = to_puny(host)?;
|
||||
|
||||
// TODO: Does SeaORM have the `exists` method?
|
||||
if emoji::Entity::find()
|
||||
.filter(emoji::Column::Name.eq(name))
|
||||
.filter(emoji::Column::Host.eq(&ascii_host))
|
||||
.one(db)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(format!(":{name}@{ascii_host}:"));
|
||||
}
|
||||
|
||||
tracing::info!("nonexistent remote custom emoji: :{name}@{ascii_host}:");
|
||||
} else {
|
||||
// local emoji
|
||||
// TODO: Does SeaORM have the `exists` method?
|
||||
if emoji::Entity::find()
|
||||
.filter(emoji::Column::Name.eq(name))
|
||||
.filter(emoji::Column::Host.is_null())
|
||||
.one(db)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(format!(":{name}:"));
|
||||
}
|
||||
|
||||
tracing::info!("nonexistent local custom emoji: :{name}:");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Ok(fetch_meta(true).await?.default_reaction)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{decode_reaction, DecodedReaction};
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
|
||||
#[test]
|
||||
fn test_decode_reaction() {
|
||||
let unicode_emoji_1 = DecodedReaction {
|
||||
reaction: "⭐".to_string(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
let unicode_emoji_2 = DecodedReaction {
|
||||
reaction: "🩷".to_string(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
|
||||
assert_eq!(decode_reaction("⭐"), unicode_emoji_1);
|
||||
assert_eq!(decode_reaction("🩷"), unicode_emoji_2);
|
||||
|
||||
assert_ne!(decode_reaction("⭐"), unicode_emoji_2);
|
||||
assert_ne!(decode_reaction("🩷"), unicode_emoji_1);
|
||||
|
||||
let unicode_emoji_3 = DecodedReaction {
|
||||
reaction: "🖖🏿".to_string(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(decode_reaction("🖖🏿"), unicode_emoji_3);
|
||||
|
||||
let local_emoji = DecodedReaction {
|
||||
reaction: ":meow_melt_tears@.:".to_string(),
|
||||
name: Some("meow_melt_tears".to_string()),
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(decode_reaction(":meow_melt_tears:"), local_emoji);
|
||||
|
||||
let remote_emoji_1 = DecodedReaction {
|
||||
reaction: ":meow_uwu@some-domain.example.org:".to_string(),
|
||||
name: Some("meow_uwu".to_string()),
|
||||
host: Some("some-domain.example.org".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
decode_reaction(":meow_uwu@some-domain.example.org:"),
|
||||
remote_emoji_1
|
||||
);
|
||||
|
||||
let remote_emoji_2 = DecodedReaction {
|
||||
reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(),
|
||||
name: Some("C++23".to_string()),
|
||||
host: Some("xn--eckwd4c7c.example.org".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
decode_reaction(":C++23@xn--eckwd4c7c.example.org:"),
|
||||
remote_emoji_2
|
||||
);
|
||||
|
||||
let invalid_reaction_1 = DecodedReaction {
|
||||
reaction: ":foo".to_string(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(decode_reaction(":foo"), invalid_reaction_1);
|
||||
|
||||
let invalid_reaction_2 = DecodedReaction {
|
||||
reaction: ":foo&@example.com:".to_string(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(decode_reaction(":foo&@example.com:"), invalid_reaction_2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// TODO: We want to get rid of this
|
||||
|
||||
use crate::database::db_conn;
|
||||
use crate::model::entity::attestation_challenge;
|
||||
use chrono::{Duration, Local};
|
||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter};
|
||||
|
||||
/// Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago
|
||||
#[crate::export]
|
||||
pub async fn remove_old_attestation_challenges() -> Result<(), DbErr> {
|
||||
let res = attestation_challenge::Entity::delete_many()
|
||||
.filter(attestation_challenge::Column::CreatedAt.lt(Local::now() - Duration::minutes(5)))
|
||||
.exec(db_conn().await?)
|
||||
.await?;
|
||||
|
||||
tracing::info!("{} attestation challenges are removed", res.rows_affected);
|
||||
|
||||
Ok(())
|
||||
}
|
90
packages/backend-rs/src/misc/system_info.rs
Normal file
90
packages/backend-rs/src/misc/system_info.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use crate::init::system_info::{system, SysinfoPoisonError};
|
||||
use sysinfo::{Disks, MemoryRefreshKind};
|
||||
|
||||
// TODO: i64 -> u64 (we can't export u64 to Node.js)
|
||||
|
||||
#[crate::export(object)]
|
||||
pub struct Cpu {
|
||||
pub model: String,
|
||||
// TODO: u16 -> usize (we can't export usize to Node.js)
|
||||
pub cores: u16,
|
||||
}
|
||||
|
||||
#[crate::export(object)]
|
||||
pub struct Memory {
|
||||
/// Total memory amount in bytes
|
||||
pub total: i64,
|
||||
/// Used memory amount in bytes
|
||||
pub used: i64,
|
||||
/// Available (for (re)use) memory amount in bytes
|
||||
pub available: i64,
|
||||
}
|
||||
|
||||
#[crate::export(object)]
|
||||
pub struct Storage {
|
||||
/// Total storage space in bytes
|
||||
pub total: i64,
|
||||
/// Used storage space in bytes
|
||||
pub used: i64,
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn cpu_info() -> Result<Cpu, SysinfoPoisonError> {
|
||||
let system_info = system()?;
|
||||
|
||||
Ok(Cpu {
|
||||
model: match system_info.cpus() {
|
||||
[] => {
|
||||
tracing::debug!("failed to get CPU info");
|
||||
"unknown".to_string()
|
||||
}
|
||||
cpus => cpus[0].brand().to_string(),
|
||||
},
|
||||
cores: system_info.cpus().len() as u16,
|
||||
})
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn cpu_usage() -> Result<f32, SysinfoPoisonError> {
|
||||
let mut system_info = system()?;
|
||||
system_info.refresh_cpu_usage();
|
||||
|
||||
let total_cpu_usage: f32 = system_info.cpus().iter().map(|cpu| cpu.cpu_usage()).sum();
|
||||
let cpu_threads = system_info.cpus().len();
|
||||
|
||||
Ok(total_cpu_usage / (cpu_threads as f32))
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn memory_usage() -> Result<Memory, SysinfoPoisonError> {
|
||||
let mut system_info = system()?;
|
||||
|
||||
system_info.refresh_memory_specifics(MemoryRefreshKind::new().with_ram());
|
||||
|
||||
Ok(Memory {
|
||||
total: system_info.total_memory() as i64,
|
||||
used: system_info.used_memory() as i64,
|
||||
available: system_info.available_memory() as i64,
|
||||
})
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn storage_usage() -> Option<Storage> {
|
||||
// Get the first disk that is actualy used.
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
let disk = disks
|
||||
.iter()
|
||||
.find(|disk| disk.available_space() > 0 && disk.total_space() > disk.available_space());
|
||||
|
||||
if let Some(disk) = disk {
|
||||
let total = disk.total_space() as i64;
|
||||
let available = disk.available_space() as i64;
|
||||
return Some(Storage {
|
||||
total,
|
||||
used: total - available,
|
||||
});
|
||||
}
|
||||
|
||||
tracing::debug!("failed to get stats");
|
||||
None
|
||||
}
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "abuse_user_report")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "access_token")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "ad")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "announcement")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "announcement_read")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
use super::sea_orm_active_enums::AntennaSrcEnum;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "antenna")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
@ -20,8 +21,6 @@ pub struct Model {
|
|||
pub src: AntennaSrcEnum,
|
||||
#[sea_orm(column_name = "userListId")]
|
||||
pub user_list_id: Option<String>,
|
||||
#[sea_orm(column_type = "JsonBinary")]
|
||||
pub keywords: Json,
|
||||
#[sea_orm(column_name = "withFile")]
|
||||
pub with_file: bool,
|
||||
pub expression: Option<String>,
|
||||
|
@ -33,10 +32,10 @@ pub struct Model {
|
|||
#[sea_orm(column_name = "userGroupJoiningId")]
|
||||
pub user_group_joining_id: Option<String>,
|
||||
pub users: Vec<String>,
|
||||
#[sea_orm(column_name = "excludeKeywords", column_type = "JsonBinary")]
|
||||
pub exclude_keywords: Json,
|
||||
#[sea_orm(column_type = "JsonBinary")]
|
||||
pub instances: Json,
|
||||
pub instances: Vec<String>,
|
||||
pub keywords: Vec<String>,
|
||||
#[sea_orm(column_name = "excludeKeywords")]
|
||||
pub exclude_keywords: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Default)]
|
||||
#[sea_orm(table_name = "antenna_note")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
#[sea_orm(column_name = "noteId")]
|
||||
pub note_id: String,
|
||||
#[sea_orm(column_name = "antennaId")]
|
||||
pub antenna_id: String,
|
||||
pub read: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::antenna::Entity",
|
||||
from = "Column::AntennaId",
|
||||
to = "super::antenna::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Antenna,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::note::Entity",
|
||||
from = "Column::NoteId",
|
||||
to = "super::note::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Note,
|
||||
}
|
||||
|
||||
impl Related<super::antenna::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Antenna.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::note::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Note.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "app")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "attestation_challenge")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "auth_session")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "blocking")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "channel")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "channel_following")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "channel_note_pining")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "clip")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "clip_note")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
|
||||
|
||||
use super::sea_orm_active_enums::DriveFileUsageHintEnum;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "drive_file")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
@ -52,6 +54,8 @@ pub struct Model {
|
|||
pub request_headers: Option<Json>,
|
||||
#[sea_orm(column_name = "requestIp")]
|
||||
pub request_ip: Option<String>,
|
||||
#[sea_orm(column_name = "usageHint")]
|
||||
pub usage_hint: Option<DriveFileUsageHintEnum>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "drive_folder")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "emoji")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "follow_request")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "following")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue