mirror of
https://git.joinfirefish.org/firefish/firefish.git
synced 2024-05-18 14:21:12 +02:00
Compare commits
485 commits
Author | SHA1 | Date | |
---|---|---|---|
2613dfd952 | |||
dfc8763f0c | |||
3d5003acc2 | |||
b8e5b8616a | |||
3a7c5fcdbb | |||
1c0d2e43b4 | |||
d9ff9101a1 | |||
58268a2c6e | |||
c4a7cd6029 | |||
2a8193fa6c | |||
ada5ff7e75 | |||
7dd3b8ec5a | |||
88890153a1 | |||
23f47aac38 | |||
abc9d58f7c | |||
ab3ca2a20b | |||
fdc77b74ae | |||
61562a0943 | |||
a298302f76 | |||
42d4f2fd79 | |||
234e5b8619 | |||
1904fbaa43 | |||
c1f5bbd2e8 | |||
282944a199 | |||
fcb4017700 | |||
18eaa915b8 | |||
52456a509e | |||
8c9a1abd32 | |||
2ee48321fd | |||
1d1e9105e5 | |||
e9287357f7 | |||
29023d7240 | |||
4e50ffeb09 | |||
2fdd165144 | |||
7e8d71003f | |||
1e313846ca | |||
ffa08748d0 | |||
5e53f9a8cf | |||
a4779f233b | |||
fd5f27eb49 | |||
3d28acb2c9 | |||
ff6ad1c2a9 | |||
20cd64281a | |||
e47f5f0cfc | |||
0ad1c581b0 | |||
ead1db27a5 | |||
12be7266db | |||
7ad4ffecf3 | |||
4e39a85708 | |||
c21bd40a5c | |||
1b537da466 | |||
8cee026c72 | |||
0ee3db7788 | |||
ff63089128 | |||
287dcece57 | |||
9298a6252d | |||
088dfd21e7 | |||
03323e40fa | |||
c6e3506bd5 | |||
128fc72778 | |||
310059f6a0 | |||
7d4d1c1fbd | |||
dbd205972f | |||
41b32c5535 | |||
56be2f034e | |||
e15bcee86c | |||
43326cdf8d | |||
7d1947792d | |||
d28fe77d9f | |||
acc13e9b10 | |||
4e31e11f81 | |||
dddd2779c0 | |||
832fc7cd1d | |||
a18ad132be | |||
4b96063c23 | |||
0de54e02f8 | |||
101e50926b | |||
9cf88f0df6 | |||
efb6cc9132 | |||
58f3eb4924 | |||
5adc0e581d | |||
c0b760cda5 | |||
eb967564f9 | |||
0085105e72 | |||
217b3ecf80 | |||
ffeeb3b444 | |||
2f00947a24 | |||
5608129913 | |||
8923e1f2a7 | |||
8765e6ba54 | |||
7c72738983 | |||
ff446de7e8 | |||
411d00a7af | |||
65a1fa870b | |||
1d25c78866 | |||
6067eaef04 | |||
92299423a3 | |||
65a8984c09 | |||
99eb364778 | |||
266c81df1e | |||
6a2e91efa1 | |||
17cbb9cd1e | |||
d6ebb55556 | |||
4dd1cff80b | |||
752c6dc75b | |||
cede0fdae2 | |||
35d706e45d | |||
9075050a67 | |||
edc2a7d890 | |||
28e2a24585 | |||
2884b2fb42 | |||
d8e1ab63c0 | |||
c2d5859755 | |||
457bd22b7b | |||
6176c09509 | |||
e9068acddd | |||
3ccacb7fce | |||
654ab006a6 | |||
1942d772db | |||
bc08f0faa9 | |||
fa35d1f4dd | |||
4db42272e7 | |||
cfa3263c46 | |||
b09e418cf6 | |||
e5a5d715b6 | |||
9ad61fe607 | |||
d6983e92aa | |||
07e2571c79 | |||
0a9289abe3 | |||
9dda7f955a | |||
a14a4a5f9c | |||
b3eb12ccff | |||
6cd511d473 | |||
a82ed86539 | |||
30ce11f9fe | |||
afb2d30e65 | |||
fb3cd43102 | |||
1cd58ce05d | |||
55fdd8b5a2 | |||
704d39f202 | |||
ef95673d50 | |||
eb443c8494 | |||
52e3b49533 | |||
11ded9491e | |||
d3b899ccc3 | |||
6d6c0fbca0 | |||
2257721fe3 | |||
cd836daa9b | |||
bf5a2c6ebb | |||
f0632d2a6b | |||
d192a7c81a | |||
847cc47fc4 | |||
59862f16b0 | |||
0e5e96c99a | |||
dbc24a0b8f | |||
1eb26263c1 | |||
639a838736 | |||
811be1022a | |||
7f277878a6 | |||
119cbe3e4f | |||
2e15165117 | |||
1b526c651e | |||
afe06edd16 | |||
3bdf4f9f9c | |||
567ba873e3 | |||
6eef39158e | |||
f81739b8d1 | |||
ff7fffc711 | |||
daade3865b | |||
7b24210bd8 | |||
d77da088f8 | |||
d6541a3ebb | |||
4fbf211e96 | |||
132615958b | |||
27c4b4c812 | |||
99f5063f4c | |||
ef706fff9d | |||
84528680df | |||
11b5f5cc17 | |||
da752a158c | |||
693c9edb10 | |||
4110842357 | |||
8656bdb185 | |||
af4426653e | |||
3750ca426b | |||
3a8ca2a2d7 | |||
ace081f163 | |||
184b0e4019 | |||
bec62cffc6 | |||
d5493f8e5d | |||
1cb64b7fa8 | |||
8f59f26aa0 | |||
6f6333f094 | |||
96cbc6799c | |||
d4f1e06535 | |||
f9e2bd2448 | |||
b07dc87af6 | |||
aa266d91e0 | |||
8f8d62aa58 | |||
d1b33ad76f | |||
eeb09028bd | |||
ded0de27c5 | |||
ac57e4c019 | |||
291990d320 | |||
2ca7bd65aa | |||
fb4e449139 | |||
084c7f1c84 | |||
421030c38f | |||
f933525856 | |||
9c8e5eabb4 | |||
4d3072929e | |||
612ce48f44 | |||
95fd20a46f | |||
3886c5624b | |||
fc7de024c6 | |||
bd88c3399f | |||
5b8a164b8d | |||
b3cc8cdb3c | |||
8373623136 | |||
d04f85d4bd | |||
33853a3a9b | |||
26a58c92df | |||
4a81106cf5 | |||
bdc5d02d27 | |||
075d326d7b | |||
0a7f16c11f | |||
3af8f86924 | |||
276cabbbe3 | |||
af14bee31f | |||
b3d1be457b | |||
347851d6bb | |||
abec71074b | |||
272e30be0c | |||
971f196627 | |||
8cc0e40d35 | |||
beeea86253 | |||
084a4bc63a | |||
cda31d3dc7 | |||
907578e8f8 | |||
2923ea86de | |||
226c990385 | |||
769f52c8ee | |||
8a00d82f36 | |||
34ed877f57 | |||
f5074f35cc | |||
a847dd55ad | |||
5382dc5da8 | |||
989e93f2a0 | |||
df81cb6a85 | |||
31168cc7b2 | |||
42886f054d | |||
1d0ea11eea | |||
24602c4745 | |||
33923a59fa | |||
8067ed4084 | |||
4277ad0b59 | |||
fc65d8c1c3 | |||
3b3d457c3e | |||
1128e243d3 | |||
39e08f57e8 | |||
09ef642905 | |||
1b8748bc8c | |||
82c98ae72f | |||
5b3f93457b | |||
4d9c0f8e7b | |||
bf2b624bc9 | |||
5261eb24b6 | |||
d440e9b388 | |||
14b285f882 | |||
baa5c402db | |||
5b01d3574f | |||
e3a98ebc72 | |||
7fe7f90350 | |||
8ed942e00f | |||
ddfdd038ad | |||
7fdd44cf8d | |||
0c4826becf | |||
ecd8e3d109 | |||
a3b156441a | |||
ecbd8a8724 | |||
442dc33a34 | |||
c8372767fa | |||
8e497b41cf | |||
bfdf73caeb | |||
5b18f9761c | |||
641ff742bb | |||
e6121946aa | |||
c6212ff8f4 | |||
d582a84c57 | |||
a7978e2b08 | |||
766bac3dee | |||
7360736966 | |||
e797849e9b | |||
ef57735e6a | |||
e7c33835b2 | |||
4e83dbd01f | |||
dd74eabae1 | |||
711618b42c | |||
510207b101 | |||
49825853c1 | |||
359fef0a42 | |||
fda81a9f91 | |||
c505c6df36 | |||
9a4a75bf92 | |||
5e5d01d407 | |||
d114b8ec1d | |||
d2471b6db7 | |||
341b43ed71 | |||
6d64358674 | |||
4992999bb7 | |||
38c0de39b9 | |||
722d090f8d | |||
b185c0c87e | |||
8c22b0d07f | |||
0f4c05a64f | |||
bc39badf51 | |||
1ee8198c6f | |||
15c32d5510 | |||
bf3f4906ac | |||
dbe10f88b0 | |||
369b1d72df | |||
e6ba0a002f | |||
37e03007f0 | |||
f66ecd0759 | |||
5ab81c2799 | |||
8a2f3b3e36 | |||
f7c576f8fb | |||
46d0679845 | |||
4536ac0678 | |||
160e7f26a6 | |||
4a0e4a4c91 | |||
a8f659ab88 | |||
64c07a2406 | |||
6e9ae06990 | |||
caae8474a6 | |||
9138c3726a | |||
425b333474 | |||
d1c76b3882 | |||
d82f212c24 | |||
f85e6ebb19 | |||
587c64a906 | |||
a4a96f0026 | |||
c0c5cb92cd | |||
fbfbffcb72 | |||
5ad9bd8ceb | |||
c345d80f4a | |||
f40c201670 | |||
c8c7abe6ef | |||
1a46d1394c | |||
32176f86b2 | |||
7d02a8852b | |||
1f8745b268 | |||
Laura Hausmann | e790d6be90 | ||
0b6d0b525f | |||
1e9e5d1c18 | |||
21e0439b5e | |||
74b843614a | |||
956592147c | |||
2bf8eab74a | |||
54b6e14621 | |||
5600799261 | |||
c47d5d70e7 | |||
f14a5bcb25 | |||
4fcd5463e9 | |||
ce0cec216c | |||
0272b08df8 | |||
a44e9f3e38 | |||
941a31c702 | |||
64ca96d759 | |||
fe1efed369 | |||
aed2e9853a | |||
50d662d62b | |||
9c3ba0c593 | |||
c2a9b028a3 | |||
cc560811cf | |||
28b9e35f2a | |||
51f9d20073 | |||
98cc23557f | |||
e5bac649c8 | |||
53dfec57a8 | |||
1b143ebfaa | |||
38cd4bafde | |||
d880601d56 | |||
c89525d151 | |||
2ccc45cfad | |||
aae505ad68 | |||
f80ee9f36d | |||
efcadf681f | |||
cd3c6a52dd | |||
a7a47f7d3b | |||
564eb08386 | |||
dc8ddff007 | |||
54f100032e | |||
98048699c1 | |||
37cf4f8361 | |||
d40db1ee7e | |||
0caba566e6 | |||
883645a581 | |||
3190f66740 | |||
8b73a1a6b3 | |||
41f9a0bda9 | |||
99f30ba01a | |||
bfcadaa094 | |||
18439f6e27 | |||
f806c47c7c | |||
b58d940e71 | |||
42f704b515 | |||
8534154792 | |||
1cfe3bfb73 | |||
ba8e044f42 | |||
79ab7bf787 | |||
23e57737a6 | |||
5c60e030e8 | |||
0512dca83c | |||
1757560f36 | |||
154ae05fb7 | |||
8e5d31b606 | |||
2d15d6dcfa | |||
171e2f3973 | |||
fd40c3dd2e | |||
3b172a7762 | |||
13b648f6bf | |||
320f933e9d | |||
fec1a800b6 | |||
22b52ac3d3 | |||
af109b45ef | |||
ab357233db | |||
6aecf12067 | |||
fefed2d4a4 | |||
bb984a8608 | |||
5de6e07dfa | |||
9a077c4beb | |||
7c827682dd | |||
0b7385e16a | |||
214e999c8c | |||
b12d7e4c63 | |||
27c46d7df9 | |||
b9c3dfbd3d | |||
5891a90f71 | |||
d82ad33730 | |||
cdea55ab25 | |||
76a45fa6ac | |||
d086b7b993 | |||
aa2b35a554 | |||
19b94b2fb8 | |||
e5a21bae13 | |||
adafd62710 | |||
217d686997 | |||
92b1f5cb5f | |||
42f6a6e3b6 | |||
40a4109c76 | |||
24e6e31b2b | |||
7441f0861a | |||
a2316b7caf | |||
322b2392de | |||
bf9ab9c1ca | |||
339cbac191 | |||
bb5349d127 | |||
93e55f146b | |||
eac0c1c47c | |||
44ad4bce32 | |||
861b66c6ad | |||
feafaffd12 | |||
7e9633a36b | |||
e67b3e8ad5 | |||
dbad2a485f | |||
3bed093344 | |||
243adaaa0d | |||
267670af96 | |||
ea60895bf8 | |||
5994a1d615 | |||
9a42745926 | |||
c0afa4a2f7 | |||
62f5c84ca6 | |||
850201ff71 | |||
6f324a3dcd | |||
93bee484bb | |||
07d39cb5ac | |||
1d3b67eafb | |||
a856523119 | |||
83e3f96ced | |||
a52340fa53 | |||
1dfc7c443b | |||
3917818c76 | |||
a07483996e |
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:
|
||||
|
|
|
@ -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
|
||||
|
|
255
.gitlab-ci.yml
Normal file
255
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,255 @@
|
|||
image: docker.io/rust:slim-bookworm
|
||||
|
||||
services:
|
||||
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
|
||||
alias: postgres
|
||||
pull_policy: if-not-present
|
||||
- name: docker.io/redis:7-alpine
|
||||
alias: redis
|
||||
pull_policy: if-not-present
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == 'firefish/firefish' || $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- locales/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
stages:
|
||||
- dependency
|
||||
- test
|
||||
- build
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: 'firefish_db'
|
||||
POSTGRES_USER: 'firefish'
|
||||
POSTGRES_PASSWORD: 'password'
|
||||
POSTGRES_HOST_AUTH_METHOD: 'trust'
|
||||
DEBIAN_FRONTEND: 'noninteractive'
|
||||
CARGO_PROFILE_DEV_OPT_LEVEL: '0'
|
||||
CARGO_PROFILE_DEV_LTO: 'off'
|
||||
CARGO_PROFILE_DEV_DEBUG: 'none'
|
||||
CARGO_TERM_COLOR: 'always'
|
||||
GIT_CLEAN_FLAGS: -ffdx -e node_modules/ -e built/ -e target/ -e packages/backend-rs/built/
|
||||
|
||||
default:
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential clang mold python3 perl nodejs postgresql-client
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
|
||||
- export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
|
||||
test:build:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: always
|
||||
needs:
|
||||
- job: cargo:clippy
|
||||
optional: true
|
||||
- job: cargo:test
|
||||
optional: true
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
test:build:backend_ts_only:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend/**/*
|
||||
- packages/firefish-js/**/*
|
||||
- packages/megalodon/**/*
|
||||
when: always
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential python3 nodejs postgresql-client
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- mkdir -p packages/backend-rs/built
|
||||
- cp packages/backend-rs/index.js packages/backend-rs/built/index.js
|
||||
- cp packages/backend-rs/index.d.ts packages/backend-rs/built/index.d.ts
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
- export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'backend' --filter 'firefish-js' --filter 'megalodon' run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
test:build:client_only:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- scripts/**/*
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/client/**/*
|
||||
- packages/firefish-js/**/*
|
||||
- packages/sw/**/*
|
||||
- locales/**/*
|
||||
when: always
|
||||
services: []
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential python3 perl nodejs
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'firefish-js' --filter 'client' --filter 'sw' run build:debug
|
||||
|
||||
build:container:
|
||||
stage: build
|
||||
image: quay.io/buildah/stable:latest
|
||||
services: []
|
||||
rules:
|
||||
- if: $BUILD == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- locales/**/*
|
||||
- scripts/copy-assets.mjs
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
when: always
|
||||
needs:
|
||||
- job: test:build
|
||||
optional: true
|
||||
- job: test:build:backend_ts_only
|
||||
optional: true
|
||||
- job: test:build:client_only
|
||||
optional: true
|
||||
variables:
|
||||
STORAGE_DRIVER: overlay2
|
||||
before_script:
|
||||
- buildah version
|
||||
- buildah prune --all --force || true
|
||||
- echo "${CI_REGISTRY_PASSWORD}" | buildah login --username "${CI_REGISTRY_USER}" --password-stdin "${CI_REGISTRY}"
|
||||
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
|
||||
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
|
||||
script:
|
||||
- |-
|
||||
buildah build \
|
||||
--platform linux/amd64 \
|
||||
--layers \
|
||||
--cache-to "${IMAGE_CACHE}" \
|
||||
--cache-from "${IMAGE_CACHE}" \
|
||||
--tag "${IMAGE_TAG}" \
|
||||
.
|
||||
- buildah inspect "${IMAGE_TAG}"
|
||||
- buildah push "${IMAGE_TAG}"
|
||||
|
||||
cargo:test:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- package.json
|
||||
when: always
|
||||
script:
|
||||
- curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin
|
||||
- cargo test --doc
|
||||
- cargo nextest run
|
||||
|
||||
cargo:clippy:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $TEST == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend-rs/**/*
|
||||
- packages/macro-rs/**/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: always
|
||||
services: []
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get install -y --no-install-recommends build-essential clang mold perl
|
||||
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
|
||||
- rustup component add clippy
|
||||
script:
|
||||
- cargo clippy -- -D warnings
|
||||
|
||||
renovate:
|
||||
stage: dependency
|
||||
image:
|
||||
name: docker.io/renovate/renovate:37-slim
|
||||
entrypoint: [""]
|
||||
rules:
|
||||
- if: $RENOVATE && $CI_PIPELINE_SOURCE == 'schedule'
|
||||
services: []
|
||||
before_script: []
|
||||
script:
|
||||
- renovate --platform gitlab --token "${API_TOKEN}" --endpoint "${CI_SERVER_URL}/api/v4" "${CI_PROJECT_PATH}"
|
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
|
||||
|
|
1874
Cargo.lock
generated
1874
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
35
Cargo.toml
35
Cargo.toml
|
@ -3,38 +3,49 @@ members = ["packages/backend-rs", "packages/macro-rs"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
macro_rs = { path = "packages/macro-rs" }
|
||||
macro-rs = { path = "packages/macro-rs" }
|
||||
|
||||
napi = { version = "2.16.2", default-features = false }
|
||||
napi-derive = "2.16.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"
|
||||
bb8 = "0.8.3"
|
||||
bcrypt = "0.15.1"
|
||||
chrono = "0.4.37"
|
||||
chrono = "0.4.38"
|
||||
convert_case = "0.6.0"
|
||||
cuid2 = "0.1.2"
|
||||
emojis = "0.6.1"
|
||||
emojis = "0.6.2"
|
||||
idna = "0.5.0"
|
||||
image = "0.25.1"
|
||||
isahc = "1.7.2"
|
||||
nom-exif = "1.2.0"
|
||||
once_cell = "1.19.0"
|
||||
openssl = "0.10.64"
|
||||
pretty_assertions = "1.4.0"
|
||||
proc-macro2 = "1.0.79"
|
||||
proc-macro2 = "1.0.82"
|
||||
quote = "1.0.36"
|
||||
rand = "0.8.5"
|
||||
redis = "0.25.3"
|
||||
redis = { version = "0.25.3", default-features = false }
|
||||
regex = "1.10.4"
|
||||
rmp-serde = "1.2.0"
|
||||
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"
|
||||
strum = "0.26.2"
|
||||
syn = "2.0.58"
|
||||
thiserror = "1.0.58"
|
||||
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
|
||||
|
||||
|
||||
|
|
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:
|
||||
|
|
|
@ -141,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
|
||||
|
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
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).
|
||||
|
|
|
@ -5,11 +5,31 @@ 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
|
||||
- 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)
|
||||
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'UserprofileJsonbToArray1714270605574',
|
||||
'DropUnusedUserprofileColumns1714259023878',
|
||||
'AntennaJsonbToArray1714192520471',
|
||||
'AddUserProfileLanguage1714888400293',
|
||||
'DropUnusedIndexes1714643926317',
|
||||
'AlterAkaType1714099399879',
|
||||
'AddDriveFileUsage1713451569342',
|
||||
'ConvertCwVarcharToText1713225866247',
|
||||
'FixChatFileConstraint1712855579316',
|
||||
|
@ -24,6 +30,68 @@ 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";
|
||||
|
@ -739,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";
|
||||
|
|
|
@ -24,6 +24,7 @@ Firefish depends on the following software.
|
|||
- `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.
|
||||
|
||||
|
@ -269,7 +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"
|
||||
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
|
||||
|
@ -325,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: Импорт/експорт на данни
|
||||
|
@ -336,6 +336,10 @@ _pages:
|
|||
title: Заглавие
|
||||
my: Моите страници
|
||||
pageSetting: Настройки на страницата
|
||||
url: Адрес на страницата
|
||||
summary: Кратко обобщение
|
||||
alignCenter: Центриране на елементите
|
||||
variables: Променливи
|
||||
_deck:
|
||||
_columns:
|
||||
notifications: Известия
|
||||
|
@ -398,7 +402,7 @@ sendMessage: Изпращане на съобщение
|
|||
jumpToPrevious: Премини към предишно
|
||||
newer: по-ново
|
||||
older: по-старо
|
||||
showLess: Покажи по-малко
|
||||
showLess: Показване на по-малко
|
||||
youGotNewFollower: те последва
|
||||
receiveFollowRequest: Заявка за последване получена
|
||||
mention: Споменаване
|
||||
|
@ -754,7 +758,7 @@ _feeds:
|
|||
general: Общи
|
||||
metadata: Метаданни
|
||||
disk: Диск
|
||||
featured: Препоръчани
|
||||
featured: Препоръчано
|
||||
yearsOld: на {age} години
|
||||
reload: Опресняване
|
||||
invites: Покани
|
||||
|
@ -940,3 +944,11 @@ 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"
|
||||
|
@ -2292,3 +2293,17 @@ 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ó
|
||||
|
|
|
@ -645,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"
|
||||
|
@ -765,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"
|
||||
|
@ -1010,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"
|
||||
|
@ -1156,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"
|
||||
|
@ -2146,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"
|
||||
|
@ -2232,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"
|
||||
|
@ -2319,3 +2322,19 @@ markLocalFilesNsfwByDefaultDescription: Indépendamment de ce réglage, les util
|
|||
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: "カラムの寄せ"
|
||||
|
@ -2059,3 +2063,13 @@ markLocalFilesNsfwByDefaultDescription: この設定が有効でも、ユーザ
|
|||
noteEditHistory: 編集履歴
|
||||
showAddFileDescriptionAtFirstPost: 説明の無い添付ファイルを投稿しようとした際に説明を書く画面を自動で開く
|
||||
antennaLimit: 各ユーザーが作れるアンテナの最大数
|
||||
inputAccountId: 'あなたのアカウントを入力してください(例: @firefish@info.firefish.dev)'
|
||||
remoteFollow: リモートフォロー
|
||||
cannotEditVisibility: 公開範囲は変更できません
|
||||
useThisAccountConfirm: このアカウントで操作を続けますか?
|
||||
getQrCode: QRコードを表示
|
||||
copyRemoteFollowUrl: リモートからフォローするURLをコピー
|
||||
foldNotification: 同じ種類の通知をまとめて表示する
|
||||
slashQuote: 繋げて引用
|
||||
mergeRenotesInTimeline: タイムラインで同じ投稿のブーストをまとめる
|
||||
mergeThreadInTimeline: タイムラインで同じスレッドの投稿をまとめる
|
||||
|
|
|
@ -564,6 +564,7 @@ deletedNote: "已删除的帖子"
|
|||
invisibleNote: "隐藏的帖子"
|
||||
enableInfiniteScroll: "滚动页面以载入更多内容"
|
||||
visibility: "可见性"
|
||||
cannotEditVisibility: "不能编辑帖子的可见性"
|
||||
poll: "调查问卷"
|
||||
useCw: "隐藏内容"
|
||||
enablePlayer: "打开播放器"
|
||||
|
@ -666,6 +667,9 @@ unclip: "移除便签"
|
|||
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?"
|
||||
public: "公开"
|
||||
i18nInfo: "Firefish 已经被志愿者们翻译成了各种语言。如果您也有兴趣,可以通过 {link} 帮助翻译。"
|
||||
i18nServerInfo: "新客户端将默认使用 {language}。"
|
||||
i18nServerChange: "改为 {language}。"
|
||||
i18nServerSet: "设定新客户端使用 {language}。"
|
||||
manageAccessTokens: "管理访问令牌"
|
||||
accountInfo: "账号信息"
|
||||
notesCount: "帖子数量"
|
||||
|
@ -878,6 +882,8 @@ driveCapOverrideCaption: "输入 0 或以下的值将容量重置为默认值。
|
|||
requireAdminForView: "您需要使用管理员账号登录才能查看。"
|
||||
isSystemAccount: "该账号由系统自动创建。请不要修改、编辑、删除或以其它方式篡改这个账号,否则可能会破坏您的服务器。"
|
||||
typeToConfirm: "输入 {x} 以确认操作"
|
||||
useThisAccountConfirm: "您想使用此帐户继续执行此操作吗?"
|
||||
inputAccountId: "请输入您的帐户(例如 @firefish@info.firefish.dev )"
|
||||
deleteAccount: "删除账号"
|
||||
document: "文档"
|
||||
numberOfPageCache: "缓存页数"
|
||||
|
@ -1787,6 +1793,7 @@ _notification:
|
|||
reacted: 回应了您的帖子
|
||||
voted: 在您的问卷调查中投了票
|
||||
renoted: 转发了您的帖子
|
||||
andCountUsers: "和其他 {count} 名用户{acted}"
|
||||
_deck:
|
||||
alwaysShowMainColumn: "总是显示主列"
|
||||
columnAlign: "列对齐"
|
||||
|
@ -1972,6 +1979,9 @@ origin: 起源
|
|||
confirm: 确认
|
||||
importZip: 导入 ZIP
|
||||
exportZip: 导出 ZIP
|
||||
getQrCode: "获取二维码"
|
||||
remoteFollow: "远程关注"
|
||||
copyRemoteFollowUrl: "复制远程关注 URL"
|
||||
emojiPackCreator: 表情包创建工具
|
||||
objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
|
||||
的端点 URL。
|
||||
|
@ -2059,3 +2069,7 @@ 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: "貼文數量"
|
||||
|
|
32
package.json
32
package.json
|
@ -1,22 +1,23 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240424",
|
||||
"version": "20240516",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.7",
|
||||
"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,7 +25,6 @@
|
|||
"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 test:ts && pnpm run test:rs",
|
||||
"test:ts": "pnpm run mocha",
|
||||
|
@ -38,21 +38,17 @@
|
|||
"clean-all": "pnpm run clean && pnpm run clean-cargo && pnpm run clean-npm"
|
||||
},
|
||||
"dependencies": {
|
||||
"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.7",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
.github
|
||||
npm
|
||||
.eslintrc
|
||||
rustfmt.toml
|
||||
yarn.lock
|
||||
*.node
|
||||
.yarn
|
||||
__test__
|
||||
renovate.json
|
|
@ -7,26 +7,33 @@ rust-version = "1.74"
|
|||
[features]
|
||||
default = []
|
||||
napi = ["dep:napi", "dep:napi-derive"]
|
||||
ci = []
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
macro_rs = { workspace = true }
|
||||
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 }
|
||||
bb8 = { workspace = true }
|
||||
bcrypt = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cuid2 = { workspace = true }
|
||||
emojis = { workspace = true }
|
||||
idna = { workspace = true }
|
||||
image = { workspace = true }
|
||||
isahc = { workspace = true }
|
||||
nom-exif = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
openssl = { workspace = true, features = ["vendored"] }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
redis = { workspace = true, default-features = false, features = ["streams", "tokio-comp"] }
|
||||
regex = { workspace = true }
|
||||
rmp-serde = { workspace = true }
|
||||
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
|
||||
|
@ -34,13 +41,18 @@ 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 }
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import test from "ava";
|
||||
|
||||
import {
|
||||
convertId,
|
||||
IdConvertType,
|
||||
nativeInitIdGenerator,
|
||||
nativeCreateId,
|
||||
nativeRandomStr,
|
||||
} from "../built/index.js";
|
||||
|
||||
test("convert to mastodon id", (t) => {
|
||||
t.is(convertId("9gf61ehcxv", IdConvertType.MastodonId), "960365976481219");
|
||||
t.is(
|
||||
convertId("9fbr9z0wbrjqyd3u", IdConvertType.MastodonId),
|
||||
"2083785058661759970208986",
|
||||
);
|
||||
t.is(
|
||||
convertId("9fbs680oyviiqrol9md73p8g", IdConvertType.MastodonId),
|
||||
"5878598648988104013828532260828151168",
|
||||
);
|
||||
});
|
||||
|
||||
test("create cuid2 with timestamp prefix", (t) => {
|
||||
nativeInitIdGenerator(16, "");
|
||||
t.not(nativeCreateId(Date.now()), nativeCreateId(Date.now()));
|
||||
t.is(nativeCreateId(Date.now()).length, 16);
|
||||
});
|
||||
|
||||
test("create random string", (t) => {
|
||||
t.not(nativeRandomStr(16), nativeRandomStr(16));
|
||||
t.is(nativeRandomStr(24).length, 24);
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
extern crate napi_build;
|
||||
|
||||
fn main() {
|
||||
// watch the version in the project root package.json
|
||||
println!("cargo:rerun-if-changed=../../package.json");
|
||||
|
||||
// napi
|
||||
napi_build::setup();
|
||||
}
|
||||
|
|
271
packages/backend-rs/index.d.ts
vendored
271
packages/backend-rs/index.d.ts
vendored
|
@ -3,6 +3,21 @@
|
|||
|
||||
/* 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
|
||||
|
@ -26,7 +41,6 @@ export interface ServerConfig {
|
|||
proxySmtp?: string
|
||||
proxyBypassHosts?: Array<string>
|
||||
allowedPrivateNetworks?: Array<string>
|
||||
/** `NapiValue` is not implemented for `u64` */
|
||||
maxFileSize?: number
|
||||
accessLog?: string
|
||||
clusterLimits?: WorkerConfigInternal
|
||||
|
@ -38,7 +52,9 @@ export interface ServerConfig {
|
|||
inboxJobPerSec?: number
|
||||
deliverJobMaxAttempts?: number
|
||||
inboxJobMaxAttempts?: number
|
||||
/** deprecated */
|
||||
logLevel?: Array<string>
|
||||
maxLogLevel?: string
|
||||
syslog?: SysLogConfig
|
||||
proxyRemoteFiles?: boolean
|
||||
mediaProxy?: string
|
||||
|
@ -148,7 +164,9 @@ export interface Config {
|
|||
inboxJobPerSec?: number
|
||||
deliverJobMaxAttempts?: number
|
||||
inboxJobMaxAttempts?: number
|
||||
/** deprecated */
|
||||
logLevel?: Array<string>
|
||||
maxLogLevel?: string
|
||||
syslog?: SysLogConfig
|
||||
proxyRemoteFiles?: boolean
|
||||
mediaProxy?: string
|
||||
|
@ -156,8 +174,8 @@ export interface Config {
|
|||
reservedUsernames?: Array<string>
|
||||
maxUserSignups?: number
|
||||
isManagedHosting?: boolean
|
||||
maxNoteLength?: number
|
||||
maxCaptionLength?: number
|
||||
maxNoteLength: number
|
||||
maxCaptionLength: number
|
||||
deepl?: DeepLConfig
|
||||
libreTranslate?: LibreTranslateConfig
|
||||
email?: EmailConfig
|
||||
|
@ -173,18 +191,6 @@ export interface Config {
|
|||
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 {
|
||||
|
@ -193,32 +199,32 @@ export interface Acct {
|
|||
}
|
||||
export function stringToAcct(acct: string): Acct
|
||||
export function acctToString(acct: Acct): string
|
||||
export function addNoteToAntenna(antennaId: string, note: Note): void
|
||||
export function greet(): void
|
||||
export function initializeRustLogger(): void
|
||||
export function showServerInfo(): void
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be blocked
|
||||
*/
|
||||
* Checks if a server is blocked.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isBlockedServer(host: string): Promise<boolean>
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be limited
|
||||
*/
|
||||
* Checks if a server is silenced.
|
||||
*
|
||||
* ## Argument
|
||||
* `host` - punycoded instance host
|
||||
*/
|
||||
export function isSilencedServer(host: string): Promise<boolean>
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host is allowlisted (this is always true if private mode is disabled)
|
||||
*/
|
||||
* 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>
|
||||
/** TODO: handle name collisions better */
|
||||
export interface NoteLikeForCheckWordMute {
|
||||
fileIds: Array<string>
|
||||
userId: string | null
|
||||
text: string | null
|
||||
cw: string | null
|
||||
renoteId: string | null
|
||||
replyId: string | null
|
||||
}
|
||||
export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<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
|
||||
|
@ -229,7 +235,20 @@ 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
|
||||
|
@ -237,6 +256,9 @@ export interface NoteLikeForGetNoteSummary {
|
|||
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>
|
||||
|
@ -266,6 +288,28 @@ export function countReactions(reactions: Record<string, number>): Record<string
|
|||
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
|
||||
|
@ -327,7 +371,6 @@ export interface Antenna {
|
|||
name: string
|
||||
src: AntennaSrcEnum
|
||||
userListId: string | null
|
||||
keywords: Json
|
||||
withFile: boolean
|
||||
expression: string | null
|
||||
notify: boolean
|
||||
|
@ -335,8 +378,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
|
||||
|
@ -993,10 +1037,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
|
||||
|
@ -1074,7 +1118,6 @@ export interface UserProfile {
|
|||
twoFactorSecret: string | null
|
||||
twoFactorEnabled: boolean
|
||||
password: string | null
|
||||
clientData: Json
|
||||
autoAcceptFollowed: boolean
|
||||
alwaysMarkNsfw: boolean
|
||||
carefulBot: boolean
|
||||
|
@ -1082,21 +1125,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
|
||||
|
@ -1122,15 +1165,151 @@ export interface Webhook {
|
|||
latestSentAt: Date | null
|
||||
latestStatus: number | null
|
||||
}
|
||||
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): void
|
||||
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]`.
|
||||
|
@ -1143,4 +1322,6 @@ export function getTimestamp(id: string): number
|
|||
export function genId(): string
|
||||
/** Generate an ID using a specific datetime */
|
||||
export function genIdAt(date: Date): string
|
||||
export function secureRndstr(length?: number | undefined | null): string
|
||||
/** Generate random string based on [thread_rng] and [Alphanumeric]. */
|
||||
export function generateSecureRandomString(length: number): string
|
||||
export function generateUserToken(): string
|
||||
|
|
|
@ -310,13 +310,22 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, watchNote, unwatchNote, ChatEvent, publishToChatStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, updateAntennasOnNewNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
|
||||
|
||||
module.exports.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.addNoteToAntenna = addNoteToAntenna
|
||||
module.exports.greet = greet
|
||||
module.exports.initializeRustLogger = initializeRustLogger
|
||||
module.exports.showServerInfo = showServerInfo
|
||||
module.exports.isBlockedServer = isBlockedServer
|
||||
module.exports.isSilencedServer = isSilencedServer
|
||||
module.exports.isAllowedServer = isAllowedServer
|
||||
|
@ -330,7 +339,11 @@ 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
|
||||
|
@ -343,6 +356,10 @@ 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
|
||||
|
@ -354,11 +371,27 @@ module.exports.RelayStatusEnum = RelayStatusEnum
|
|||
module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
|
||||
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
|
||||
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
|
||||
module.exports.updateAntennasOnNewNote = updateAntennasOnNewNote
|
||||
module.exports.fetchNodeinfo = fetchNodeinfo
|
||||
module.exports.nodeinfo_2_1 = nodeinfo_2_1
|
||||
module.exports.nodeinfo_2_0 = nodeinfo_2_0
|
||||
module.exports.Protocol = Protocol
|
||||
module.exports.Inbound = Inbound
|
||||
module.exports.Outbound = Outbound
|
||||
module.exports.watchNote = watchNote
|
||||
module.exports.unwatchNote = unwatchNote
|
||||
module.exports.PushNotificationKind = PushNotificationKind
|
||||
module.exports.sendPushNotification = sendPushNotification
|
||||
module.exports.publishToChannelStream = publishToChannelStream
|
||||
module.exports.ChatEvent = ChatEvent
|
||||
module.exports.publishToChatStream = publishToChatStream
|
||||
module.exports.ChatIndexEvent = ChatIndexEvent
|
||||
module.exports.publishToChatIndexStream = publishToChatIndexStream
|
||||
module.exports.publishToBroadcastStream = publishToBroadcastStream
|
||||
module.exports.publishToGroupChatStream = publishToGroupChatStream
|
||||
module.exports.publishToModerationStream = publishToModerationStream
|
||||
module.exports.getTimestamp = getTimestamp
|
||||
module.exports.genId = genId
|
||||
module.exports.genIdAt = genIdAt
|
||||
module.exports.secureRndstr = secureRndstr
|
||||
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 --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",
|
||||
];
|
|
@ -1,4 +1,5 @@
|
|||
pub use server::CONFIG;
|
||||
|
||||
pub mod constant;
|
||||
pub mod environment;
|
||||
pub mod server;
|
||||
|
|
|
@ -3,6 +3,8 @@ use serde::Deserialize;
|
|||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
pub const VERSION: &str = macro_rs::read_version_from_package_json!();
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, use_nullable = false)]
|
||||
|
@ -22,7 +24,7 @@ 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<WorkerConfigInternal>,
|
||||
|
@ -36,8 +38,11 @@ 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>,
|
||||
|
@ -197,7 +202,11 @@ pub struct Config {
|
|||
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>,
|
||||
|
@ -205,8 +214,8 @@ pub struct Config {
|
|||
pub reserved_usernames: Option<Vec<String>>,
|
||||
pub max_user_signups: Option<u32>,
|
||||
pub is_managed_hosting: Option<bool>,
|
||||
pub max_note_length: Option<u32>,
|
||||
pub max_caption_length: Option<u32>,
|
||||
pub max_note_length: u32,
|
||||
pub max_caption_length: u32,
|
||||
pub deepl: Option<DeepLConfig>,
|
||||
pub libre_translate: Option<LibreTranslateConfig>,
|
||||
pub email: Option<EmailConfig>,
|
||||
|
@ -224,34 +233,6 @@ pub struct Config {
|
|||
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 {
|
||||
|
@ -273,28 +254,10 @@ fn read_config_file() -> ServerConfig {
|
|||
data
|
||||
}
|
||||
|
||||
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]
|
||||
fn load_config() -> Config {
|
||||
pub fn load_config() -> Config {
|
||||
let server_config = read_config_file();
|
||||
let version = read_meta().version;
|
||||
let manifest = read_manifest();
|
||||
let version = VERSION.to_owned();
|
||||
let url = url::Url::parse(&server_config.url).expect("Config url is invalid");
|
||||
let hostname = url
|
||||
.host_str()
|
||||
|
@ -346,6 +309,7 @@ fn load_config() -> Config {
|
|||
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,
|
||||
|
@ -353,8 +317,8 @@ fn load_config() -> Config {
|
|||
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,
|
||||
max_caption_length: server_config.max_caption_length,
|
||||
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,
|
||||
|
@ -371,7 +335,6 @@ fn load_config() -> Config {
|
|||
redis_key_prefix,
|
||||
scheme,
|
||||
ws_scheme,
|
||||
client_entry: manifest,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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,6 +1,8 @@
|
|||
pub use postgresql::db_conn;
|
||||
pub use redis::key as redis_key;
|
||||
pub use redis::redis_conn;
|
||||
pub use redis::RedisConnError;
|
||||
|
||||
pub mod cache;
|
||||
pub mod postgresql;
|
||||
pub mod redis;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use crate::config::CONFIG;
|
||||
use sea_orm::{Database, DbConn, DbErr};
|
||||
use once_cell::sync::OnceCell;
|
||||
use sea_orm::{ConnectOptions, Database, DbConn, DbErr};
|
||||
use tracing::log::LevelFilter;
|
||||
|
||||
static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new();
|
||||
static DB_CONN: OnceCell<DbConn> = OnceCell::new();
|
||||
|
||||
async fn init_database() -> Result<&'static DbConn, DbErr> {
|
||||
let database_uri = format!(
|
||||
|
@ -12,7 +14,13 @@ async fn init_database() -> Result<&'static DbConn, DbErr> {
|
|||
CONFIG.db.port,
|
||||
CONFIG.db.db,
|
||||
);
|
||||
let conn = Database::connect(database_uri).await?;
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,50 @@
|
|||
use crate::config::CONFIG;
|
||||
use redis::{Client, Connection, RedisError};
|
||||
use async_trait::async_trait;
|
||||
use bb8::{ManageConnection, Pool, PooledConnection, RunError};
|
||||
use redis::{aio::MultiplexedConnection, Client, ErrorKind, IntoConnectionInfo, RedisError};
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
static REDIS_CLIENT: once_cell::sync::OnceCell<Client> = once_cell::sync::OnceCell::new();
|
||||
/// A `bb8::ManageConnection` for `redis::Client::get_multiplexed_async_connection`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RedisConnectionManager {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
fn init_redis() -> Result<Client, RedisError> {
|
||||
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()];
|
||||
|
||||
|
@ -26,18 +67,44 @@ fn init_redis() -> Result<Client, RedisError> {
|
|||
params.concat()
|
||||
};
|
||||
|
||||
Client::open(redis_url)
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn redis_conn() -> Result<Connection, RedisError> {
|
||||
match REDIS_CLIENT.get() {
|
||||
Some(client) => Ok(client.get_connection()?),
|
||||
None => init_redis()?.get_connection(),
|
||||
#[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)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// prefix redis key
|
||||
#[inline]
|
||||
pub fn key(key: impl ToString) -> String {
|
||||
format!("{}:{}", CONFIG.redis_key_prefix, key.to_string())
|
||||
}
|
||||
|
@ -46,23 +113,26 @@ pub fn key(key: impl ToString) -> String {
|
|||
mod unit_test {
|
||||
use super::redis_conn;
|
||||
use pretty_assertions::assert_eq;
|
||||
use redis::Commands;
|
||||
use redis::AsyncCommands;
|
||||
|
||||
#[test]
|
||||
fn connect() {
|
||||
assert!(redis_conn().is_ok());
|
||||
assert!(redis_conn().is_ok());
|
||||
#[tokio::test]
|
||||
async fn connect() {
|
||||
assert!(redis_conn().await.is_ok());
|
||||
assert!(redis_conn().await.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access() {
|
||||
let mut redis = redis_conn().unwrap();
|
||||
#[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).unwrap(), "OK");
|
||||
assert_eq!(redis.get::<&str, String>(key).unwrap(), value);
|
||||
assert_eq!(redis.del::<&str, u32>(key).unwrap(), 1);
|
||||
assert_eq!(
|
||||
redis.set::<&str, &str, String>(key, value).await.unwrap(),
|
||||
"OK"
|
||||
);
|
||||
assert_eq!(redis.get::<&str, String>(key).await.unwrap(), value);
|
||||
assert_eq!(redis.del::<&str, u32>(key).await.unwrap(), 1);
|
||||
}
|
||||
}
|
||||
|
|
105
packages/backend-rs/src/federation/acct.rs
Normal file
105
packages/backend-rs/src/federation/acct.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[crate::export(object)]
|
||||
pub struct Acct {
|
||||
pub username: String,
|
||||
pub host: Option<String>,
|
||||
}
|
||||
|
||||
impl FromStr for Acct {
|
||||
type Err = ();
|
||||
|
||||
/// This never throw errors. Feel free to `.unwrap()` the result.
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let split: Vec<&str> = if let Some(stripped) = value.strip_prefix('@') {
|
||||
stripped
|
||||
} else {
|
||||
value
|
||||
}
|
||||
.split('@')
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
username: split[0].to_string(),
|
||||
host: if split.len() == 1 {
|
||||
None
|
||||
} else {
|
||||
Some(split[1].to_string())
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Acct {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let result = match &self.host {
|
||||
Some(host) => format!("{}@{}", self.username, host),
|
||||
None => self.username.clone(),
|
||||
};
|
||||
write!(f, "{result}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Acct> for String {
|
||||
fn from(value: Acct) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[crate::ts_only_warn("Use `acct.parse().unwrap()` or `Acct::from_str(acct).unwrap()` instead.")]
|
||||
#[crate::export]
|
||||
pub fn string_to_acct(acct: &str) -> Acct {
|
||||
Acct::from_str(acct).unwrap()
|
||||
}
|
||||
|
||||
#[crate::ts_only_warn("Use `acct.to_string()` instead.")]
|
||||
#[crate::export]
|
||||
pub fn acct_to_string(acct: &Acct) -> String {
|
||||
acct.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::Acct;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_acct_to_string() {
|
||||
let remote_acct = Acct {
|
||||
username: "firefish".to_string(),
|
||||
host: Some("example.com".to_string()),
|
||||
};
|
||||
let local_acct = Acct {
|
||||
username: "MisakaMikoto".to_string(),
|
||||
host: None,
|
||||
};
|
||||
|
||||
assert_eq!(remote_acct.to_string(), "firefish@example.com");
|
||||
assert_ne!(remote_acct.to_string(), "mastodon@example.com");
|
||||
assert_eq!(local_acct.to_string(), "MisakaMikoto");
|
||||
assert_ne!(local_acct.to_string(), "ShiraiKuroko");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_to_acct() {
|
||||
let remote_acct = Acct {
|
||||
username: "firefish".to_string(),
|
||||
host: Some("example.com".to_string()),
|
||||
};
|
||||
let local_acct = Acct {
|
||||
username: "MisakaMikoto".to_string(),
|
||||
host: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Acct::from_str("@firefish@example.com").unwrap(),
|
||||
remote_acct
|
||||
);
|
||||
assert_eq!(Acct::from_str("firefish@example.com").unwrap(), remote_acct);
|
||||
assert_eq!(Acct::from_str("@MisakaMikoto").unwrap(), local_acct);
|
||||
assert_eq!(Acct::from_str("MisakaMikoto").unwrap(), local_acct);
|
||||
}
|
||||
}
|
1
packages/backend-rs/src/federation/mod.rs
Normal file
1
packages/backend-rs/src/federation/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod acct;
|
20
packages/backend-rs/src/init/greet.rs
Normal file
20
packages/backend-rs/src/init/greet.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
use crate::config::server::VERSION;
|
||||
|
||||
const GREETING_MESSAGE: &str = "\
|
||||
███████╗██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄
|
||||
██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄
|
||||
█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄
|
||||
██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄
|
||||
██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀
|
||||
Firefish is an open-source decentralized microblogging platform.
|
||||
If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish
|
||||
";
|
||||
|
||||
#[crate::export]
|
||||
pub fn greet() {
|
||||
println!("{}", GREETING_MESSAGE);
|
||||
|
||||
tracing::info!("Welcome to Firefish!");
|
||||
tracing::info!("Firefish {VERSION}");
|
||||
}
|
51
packages/backend-rs/src/init/log.rs
Normal file
51
packages/backend-rs/src/init/log.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use crate::config::CONFIG;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[crate::export(js_name = "initializeRustLogger")]
|
||||
pub fn initialize_logger() {
|
||||
let mut builder = FmtSubscriber::builder();
|
||||
|
||||
if let Some(max_level) = &CONFIG.max_log_level {
|
||||
builder = builder.with_max_level(match max_level.as_str() {
|
||||
"error" => Level::ERROR,
|
||||
"warning" => Level::WARN,
|
||||
"info" => Level::INFO,
|
||||
"debug" => Level::DEBUG,
|
||||
"trace" => Level::TRACE,
|
||||
_ => Level::INFO, // Fallback
|
||||
});
|
||||
} else if let Some(levels) = &CONFIG.log_level {
|
||||
// `logLevel` config is Deprecated
|
||||
if levels.contains(&"trace".to_string()) {
|
||||
builder = builder.with_max_level(Level::TRACE);
|
||||
} else if levels.contains(&"debug".to_string()) {
|
||||
builder = builder.with_max_level(Level::DEBUG);
|
||||
} else if levels.contains(&"info".to_string()) {
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
} else if levels.contains(&"warning".to_string()) {
|
||||
builder = builder.with_max_level(Level::WARN);
|
||||
} else if levels.contains(&"error".to_string()) {
|
||||
builder = builder.with_max_level(Level::ERROR);
|
||||
} else {
|
||||
// Fallback
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
}
|
||||
} else {
|
||||
// Fallback
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
};
|
||||
|
||||
let subscriber = builder
|
||||
.without_time()
|
||||
.with_level(true)
|
||||
.with_ansi(true)
|
||||
.with_target(true)
|
||||
.with_thread_names(true)
|
||||
.with_line_number(true)
|
||||
.log_internal_errors(true)
|
||||
.compact()
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("Failed to initialize the logger");
|
||||
}
|
3
packages/backend-rs/src/init/mod.rs
Normal file
3
packages/backend-rs/src/init/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod greet;
|
||||
pub mod log;
|
||||
pub mod system_info;
|
39
packages/backend-rs/src/init/system_info.rs
Normal file
39
packages/backend-rs/src/init/system_info.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
|
||||
use sysinfo::System;
|
||||
|
||||
pub type SysinfoPoisonError = PoisonError<MutexGuard<'static, System>>;
|
||||
|
||||
// TODO: handle this in a more proper way when we move the entry point to backend-rs
|
||||
pub fn system() -> Result<MutexGuard<'static, System>, SysinfoPoisonError> {
|
||||
pub static SYSTEM: OnceLock<Mutex<System>> = OnceLock::new();
|
||||
SYSTEM.get_or_init(|| Mutex::new(System::new_all())).lock()
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub fn show_server_info() -> Result<(), SysinfoPoisonError> {
|
||||
let system_info = system()?;
|
||||
|
||||
tracing::info!(
|
||||
"Hostname: {}",
|
||||
System::host_name().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"OS: {}",
|
||||
System::long_os_version().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"Kernel: {}",
|
||||
System::kernel_version().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"CPU architecture: {}",
|
||||
System::cpu_arch().unwrap_or("unknown".to_string())
|
||||
);
|
||||
tracing::info!("CPU threads: {}", system_info.cpus().len());
|
||||
tracing::info!("Total memory: {} MiB", system_info.total_memory() / 1048576);
|
||||
tracing::info!("Free memory: {} MiB", system_info.free_memory() / 1048576);
|
||||
tracing::info!("Total swap: {} MiB", system_info.total_swap() / 1048576);
|
||||
tracing::info!("Free swap: {} MiB", system_info.free_swap() / 1048576);
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
use crate::database::{redis_conn, redis_key};
|
||||
use crate::model::entity::note;
|
||||
use crate::service::stream;
|
||||
use crate::util::id::{get_timestamp, InvalidIdErr};
|
||||
use redis::{streams::StreamMaxlen, Commands, RedisError};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisErr(#[from] RedisError),
|
||||
#[error("Invalid ID: {0}")]
|
||||
InvalidIdErr(#[from] InvalidIdErr),
|
||||
#[error("Stream error: {0}")]
|
||||
StreamErr(#[from] stream::Error),
|
||||
}
|
||||
|
||||
type Note = note::Model;
|
||||
|
||||
#[crate::export]
|
||||
pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error> {
|
||||
// for timeline API
|
||||
redis_conn()?.xadd_maxlen(
|
||||
redis_key(format!("antennaTimeline:{}", antenna_id)),
|
||||
StreamMaxlen::Approx(200),
|
||||
format!("{}-*", get_timestamp(¬e.id)?),
|
||||
&[("note", ¬e.id)],
|
||||
)?;
|
||||
|
||||
// for streaming API
|
||||
Ok(stream::antenna::publish(antenna_id, note)?)
|
||||
}
|
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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
use crate::misc::meta::fetch_meta;
|
||||
use sea_orm::DbErr;
|
||||
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be blocked
|
||||
*/
|
||||
/// 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)
|
||||
|
@ -16,10 +16,10 @@ pub async fn is_blocked_server(host: &str) -> Result<bool, DbErr> {
|
|||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host should be limited
|
||||
*/
|
||||
/// 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)
|
||||
|
@ -31,10 +31,11 @@ pub async fn is_silenced_server(host: &str) -> Result<bool, DbErr> {
|
|||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param host punycoded instance host
|
||||
* @returns whether the given host is allowlisted (this is always true if private mode is disabled)
|
||||
*/
|
||||
/// 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?;
|
||||
|
|
|
@ -1,80 +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};
|
||||
|
||||
/// TODO: handle name collisions better
|
||||
#[crate::export(object, js_name = "NoteLikeForCheckWordMute")]
|
||||
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());
|
||||
|
@ -83,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| {
|
||||
|
@ -103,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()]));
|
||||
}
|
||||
}
|
||||
|
|
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)
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
/// TODO: handle name collisions better
|
||||
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>,
|
||||
|
|
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
pub mod acct;
|
||||
pub mod add_note_to_antenna;
|
||||
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 redis_cache;
|
||||
pub mod remove_old_attestation_challenges;
|
||||
pub mod system_info;
|
||||
|
|
|
@ -97,6 +97,8 @@ pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Resul
|
|||
{
|
||||
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?
|
||||
|
@ -109,6 +111,8 @@ pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Resul
|
|||
{
|
||||
return Ok(format!(":{name}:"));
|
||||
}
|
||||
|
||||
tracing::info!("nonexistent local custom emoji: :{name}:");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
use crate::database::{redis_conn, redis_key};
|
||||
use redis::{Commands, RedisError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisError(#[from] RedisError),
|
||||
#[error("Data serialization error: {0}")]
|
||||
SerializeError(#[from] rmp_serde::encode::Error),
|
||||
#[error("Data deserialization error: {0}")]
|
||||
DeserializeError(#[from] rmp_serde::decode::Error),
|
||||
}
|
||||
|
||||
pub fn set_cache<V: for<'a> Deserialize<'a> + Serialize>(
|
||||
key: &str,
|
||||
value: &V,
|
||||
expire_seconds: u64,
|
||||
) -> Result<(), Error> {
|
||||
redis_conn()?.set_ex(
|
||||
redis_key(key),
|
||||
rmp_serde::encode::to_vec(&value)?,
|
||||
expire_seconds,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_cache<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
|
||||
let serialized_value: Option<Vec<u8>> = redis_conn()?.get(redis_key(key))?;
|
||||
Ok(match serialized_value {
|
||||
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{get_cache, set_cache};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
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_cache(key_1, &value_1, 1).unwrap();
|
||||
set_cache(key_2, &value_2, 1).unwrap();
|
||||
set_cache(key_3, &value_3, 1).unwrap();
|
||||
|
||||
let cached_value_1: Vec<i32> = get_cache(key_1).unwrap().unwrap();
|
||||
let cached_value_2: String = get_cache(key_2).unwrap().unwrap();
|
||||
let cached_value_3: Data = get_cache(key_3).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_cache(key_1).unwrap();
|
||||
let expired_value_2: Option<Vec<i32>> = get_cache(key_2).unwrap();
|
||||
let expired_value_3: Option<Vec<i32>> = get_cache(key_3).unwrap();
|
||||
|
||||
assert!(expired_value_1.is_none());
|
||||
assert!(expired_value_2.is_none());
|
||||
assert!(expired_value_3.is_none());
|
||||
}
|
||||
}
|
|
@ -8,10 +8,12 @@ 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> {
|
||||
attestation_challenge::Entity::delete_many()
|
||||
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
|
||||
}
|
|
@ -21,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>,
|
||||
|
@ -34,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)]
|
||||
|
|
|
@ -71,14 +71,14 @@ pub struct Model {
|
|||
pub drive_capacity_override_mb: Option<i32>,
|
||||
#[sea_orm(column_name = "movedToUri")]
|
||||
pub moved_to_uri: Option<String>,
|
||||
#[sea_orm(column_name = "alsoKnownAs", column_type = "Text", nullable)]
|
||||
pub also_known_as: Option<String>,
|
||||
#[sea_orm(column_name = "speakAsCat")]
|
||||
pub speak_as_cat: bool,
|
||||
#[sea_orm(column_name = "emojiModPerm")]
|
||||
pub emoji_mod_perm: UserEmojimodpermEnum,
|
||||
#[sea_orm(column_name = "isIndexable")]
|
||||
pub is_indexable: bool,
|
||||
#[sea_orm(column_name = "alsoKnownAs")]
|
||||
pub also_known_as: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
|
@ -32,8 +32,6 @@ pub struct Model {
|
|||
#[sea_orm(column_name = "twoFactorEnabled")]
|
||||
pub two_factor_enabled: bool,
|
||||
pub password: Option<String>,
|
||||
#[sea_orm(column_name = "clientData", column_type = "JsonBinary")]
|
||||
pub client_data: Json,
|
||||
#[sea_orm(column_name = "autoAcceptFollowed")]
|
||||
pub auto_accept_followed: bool,
|
||||
#[sea_orm(column_name = "alwaysMarkNsfw")]
|
||||
|
@ -48,14 +46,10 @@ pub struct Model {
|
|||
pub use_password_less_login: bool,
|
||||
#[sea_orm(column_name = "pinnedPageId", unique)]
|
||||
pub pinned_page_id: Option<String>,
|
||||
#[sea_orm(column_type = "JsonBinary")]
|
||||
pub room: Json,
|
||||
#[sea_orm(column_name = "injectFeaturedNote")]
|
||||
pub inject_featured_note: bool,
|
||||
#[sea_orm(column_name = "enableWordMute")]
|
||||
pub enable_word_mute: bool,
|
||||
#[sea_orm(column_name = "mutedWords", column_type = "JsonBinary")]
|
||||
pub muted_words: Json,
|
||||
#[sea_orm(column_name = "mutingNotificationTypes")]
|
||||
pub muting_notification_types: Vec<UserProfileMutingnotificationtypesEnum>,
|
||||
#[sea_orm(column_name = "noCrawle")]
|
||||
|
@ -64,8 +58,6 @@ pub struct Model {
|
|||
pub receive_announcement_email: bool,
|
||||
#[sea_orm(column_name = "emailNotificationTypes", column_type = "JsonBinary")]
|
||||
pub email_notification_types: Json,
|
||||
#[sea_orm(column_name = "mutedInstances", column_type = "JsonBinary")]
|
||||
pub muted_instances: Json,
|
||||
#[sea_orm(column_name = "publicReactions")]
|
||||
pub public_reactions: bool,
|
||||
#[sea_orm(column_name = "ffVisibility")]
|
||||
|
@ -78,6 +70,11 @@ pub struct Model {
|
|||
pub is_indexable: bool,
|
||||
#[sea_orm(column_name = "mutedPatterns")]
|
||||
pub muted_patterns: Vec<String>,
|
||||
#[sea_orm(column_name = "mutedInstances")]
|
||||
pub muted_instances: Vec<String>,
|
||||
#[sea_orm(column_name = "mutedWords")]
|
||||
pub muted_words: Vec<String>,
|
||||
pub lang: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
78
packages/backend-rs/src/service/antenna.rs
Normal file
78
packages/backend-rs/src/service/antenna.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use crate::database::cache;
|
||||
use crate::database::{db_conn, redis_conn, redis_key, RedisConnError};
|
||||
use crate::federation::acct::Acct;
|
||||
use crate::misc::check_hit_antenna::{check_hit_antenna, AntennaCheckError};
|
||||
use crate::model::entity::{antenna, note};
|
||||
use crate::service::stream;
|
||||
use crate::util::id::{get_timestamp, InvalidIdErr};
|
||||
use redis::{streams::StreamMaxlen, AsyncCommands, RedisError};
|
||||
use sea_orm::{DbErr, EntityTrait};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
DbErr(#[from] DbErr),
|
||||
#[error("Cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("Redis error: {0}")]
|
||||
RedisErr(#[from] RedisError),
|
||||
#[error("Redis connection error: {0}")]
|
||||
RedisConnErr(#[from] RedisConnError),
|
||||
#[error("Invalid ID: {0}")]
|
||||
InvalidIdErr(#[from] InvalidIdErr),
|
||||
#[error("Stream error: {0}")]
|
||||
StreamErr(#[from] stream::Error),
|
||||
#[error("Failed to check if the note should be added to antenna: {0}")]
|
||||
AntennaCheckErr(#[from] AntennaCheckError),
|
||||
}
|
||||
|
||||
// https://github.com/napi-rs/napi-rs/issues/2060
|
||||
type Antenna = antenna::Model;
|
||||
type Note = note::Model;
|
||||
|
||||
// TODO?: it might be better to store this directly in memory
|
||||
// (like fetch_meta) instead of Redis as it's used so much
|
||||
async fn antennas() -> Result<Vec<Antenna>, Error> {
|
||||
const CACHE_KEY: &str = "antennas";
|
||||
|
||||
Ok(cache::get::<Vec<Antenna>>(CACHE_KEY).await?.unwrap_or({
|
||||
let antennas = antenna::Entity::find().all(db_conn().await?).await?;
|
||||
cache::set(CACHE_KEY, &antennas, 5 * 60).await?;
|
||||
antennas
|
||||
}))
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn update_antennas_on_new_note(
|
||||
note: Note,
|
||||
note_author: &Acct,
|
||||
note_muted_users: Vec<String>,
|
||||
) -> Result<(), Error> {
|
||||
// TODO: do this in parallel
|
||||
for antenna in antennas().await?.iter() {
|
||||
if note_muted_users.contains(&antenna.user_id) {
|
||||
continue;
|
||||
}
|
||||
if check_hit_antenna(antenna, note.clone(), note_author).await? {
|
||||
add_note_to_antenna(&antenna.id, ¬e).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_note_to_antenna(antenna_id: &str, note: &Note) -> Result<(), Error> {
|
||||
// for timeline API
|
||||
redis_conn()
|
||||
.await?
|
||||
.xadd_maxlen(
|
||||
redis_key(format!("antennaTimeline:{}", antenna_id)),
|
||||
StreamMaxlen::Approx(200),
|
||||
format!("{}-*", get_timestamp(¬e.id)?),
|
||||
&[("note", ¬e.id)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// for streaming API
|
||||
Ok(stream::antenna::publish(antenna_id.to_string(), note).await?)
|
||||
}
|
|
@ -1,2 +1,5 @@
|
|||
pub mod antenna;
|
||||
pub mod nodeinfo;
|
||||
pub mod note;
|
||||
pub mod push_notification;
|
||||
pub mod stream;
|
||||
|
|
161
packages/backend-rs/src/service/nodeinfo/fetch.rs
Normal file
161
packages/backend-rs/src/service/nodeinfo/fetch.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use crate::service::nodeinfo::schema::*;
|
||||
use crate::util::http_client;
|
||||
use isahc::AsyncReadResponseExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Http client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
#[error("Http error: {0}")]
|
||||
HttpErr(#[from] isahc::Error),
|
||||
#[error("Bad status: {0}")]
|
||||
BadStatus(String),
|
||||
#[error("Failed to parse response body as text: {0}")]
|
||||
ResponseErr(#[from] std::io::Error),
|
||||
#[error("Failed to parse response body as json: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
#[error("No nodeinfo provided")]
|
||||
MissingNodeinfo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct NodeinfoLinks {
|
||||
links: Vec<NodeinfoLink>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct NodeinfoLink {
|
||||
rel: String,
|
||||
href: String,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wellknown_nodeinfo_url(host: &str) -> String {
|
||||
format!("https://{}/.well-known/nodeinfo", host)
|
||||
}
|
||||
|
||||
async fn fetch_nodeinfo_links(host: &str) -> Result<NodeinfoLinks, Error> {
|
||||
let client = http_client::client()?;
|
||||
let wellknown_url = wellknown_nodeinfo_url(host);
|
||||
let mut wellknown_response = client.get_async(&wellknown_url).await?;
|
||||
|
||||
if !wellknown_response.status().is_success() {
|
||||
tracing::debug!("{:#?}", wellknown_response.body());
|
||||
return Err(Error::BadStatus(format!(
|
||||
"{} returned {}",
|
||||
wellknown_url,
|
||||
wellknown_response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str(&wellknown_response.text().await?)?)
|
||||
}
|
||||
|
||||
fn check_nodeinfo_link(links: NodeinfoLinks) -> Result<String, Error> {
|
||||
for link in links.links {
|
||||
if link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.1"
|
||||
|| link.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
{
|
||||
return Ok(link.href);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::MissingNodeinfo)
|
||||
}
|
||||
|
||||
async fn fetch_nodeinfo_impl(nodeinfo_link: &str) -> Result<Nodeinfo20, Error> {
|
||||
let client = http_client::client()?;
|
||||
let mut response = client.get_async(nodeinfo_link).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
tracing::debug!("{:#?}", response.body());
|
||||
return Err(Error::BadStatus(format!(
|
||||
"{} returned {}",
|
||||
nodeinfo_link,
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str(&response.text().await?)?)
|
||||
}
|
||||
|
||||
// for napi export
|
||||
type Nodeinfo = Nodeinfo20;
|
||||
|
||||
#[crate::export]
|
||||
pub async fn fetch_nodeinfo(host: &str) -> Result<Nodeinfo, Error> {
|
||||
tracing::info!("fetching from {}", host);
|
||||
let links = fetch_nodeinfo_links(host).await?;
|
||||
let nodeinfo_link = check_nodeinfo_link(links)?;
|
||||
fetch_nodeinfo_impl(&nodeinfo_link).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{check_nodeinfo_link, fetch_nodeinfo, NodeinfoLink, NodeinfoLinks};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_check_nodeinfo_link() {
|
||||
let links_1 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy".to_string(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
|
||||
href: "https://example.com/real".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
check_nodeinfo_link(links_1).unwrap(),
|
||||
"https://example.com/real"
|
||||
);
|
||||
|
||||
let links_2 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy".to_string(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".to_string(),
|
||||
href: "https://example.com/real".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
check_nodeinfo_link(links_2).unwrap(),
|
||||
"https://example.com/real"
|
||||
);
|
||||
|
||||
let links_3 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy/2.0".to_string(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.1".to_string(),
|
||||
href: "https://example.com/dummy/2.1".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
check_nodeinfo_link(links_3).expect_err("No nodeinfo");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_nodeinfo() {
|
||||
assert_eq!(
|
||||
fetch_nodeinfo("info.firefish.dev")
|
||||
.await
|
||||
.unwrap()
|
||||
.software
|
||||
.name,
|
||||
"firefish"
|
||||
);
|
||||
}
|
||||
}
|
142
packages/backend-rs/src/service/nodeinfo/generate.rs
Normal file
142
packages/backend-rs/src/service/nodeinfo/generate.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::config::CONFIG;
|
||||
use crate::database::cache;
|
||||
use crate::database::db_conn;
|
||||
use crate::misc::meta::fetch_meta;
|
||||
use crate::model::entity::{note, user};
|
||||
use crate::service::nodeinfo::schema::*;
|
||||
use sea_orm::{ColumnTrait, DbErr, EntityTrait, PaginatorTrait, QueryFilter};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
DbErr(#[from] DbErr),
|
||||
#[error("Cache error: {0}")]
|
||||
CacheErr(#[from] cache::Error),
|
||||
#[error("Failed to serialize nodeinfo to JSON: {0}")]
|
||||
JsonErr(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> {
|
||||
let db = db_conn().await?;
|
||||
|
||||
let now = chrono::Local::now().naive_local();
|
||||
const MONTH: chrono::TimeDelta = chrono::Duration::seconds(2592000000);
|
||||
const HALF_YEAR: chrono::TimeDelta = chrono::Duration::seconds(15552000000);
|
||||
|
||||
let local_users = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.count(db);
|
||||
let local_active_halfyear = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.filter(user::Column::LastActiveDate.gt(now - HALF_YEAR))
|
||||
.count(db);
|
||||
let local_active_month = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.filter(user::Column::LastActiveDate.gt(now - MONTH))
|
||||
.count(db);
|
||||
let local_posts = note::Entity::find()
|
||||
.filter(note::Column::UserHost.is_null())
|
||||
.count(db);
|
||||
|
||||
tokio::try_join!(
|
||||
local_users,
|
||||
local_active_halfyear,
|
||||
local_active_month,
|
||||
local_posts
|
||||
)
|
||||
}
|
||||
|
||||
async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
|
||||
let (local_users, local_active_halfyear, local_active_month, local_posts) =
|
||||
statistics().await?;
|
||||
let meta = fetch_meta(true).await?;
|
||||
let metadata = HashMap::from([
|
||||
(
|
||||
"nodeName".to_string(),
|
||||
json!(meta.name.unwrap_or(CONFIG.host.clone())),
|
||||
),
|
||||
("nodeDescription".to_string(), json!(meta.description)),
|
||||
("repositoryUrl".to_string(), json!(meta.repository_url)),
|
||||
(
|
||||
"enableLocalTimeline".to_string(),
|
||||
json!(!meta.disable_local_timeline),
|
||||
),
|
||||
(
|
||||
"enableRecommendedTimeline".to_string(),
|
||||
json!(!meta.disable_recommended_timeline),
|
||||
),
|
||||
(
|
||||
"enableGlobalTimeline".to_string(),
|
||||
json!(!meta.disable_global_timeline),
|
||||
),
|
||||
(
|
||||
"enableGuestTimeline".to_string(),
|
||||
json!(meta.enable_guest_timeline),
|
||||
),
|
||||
(
|
||||
"maintainer".to_string(),
|
||||
json!({"name":meta.maintainer_name,"email":meta.maintainer_email}),
|
||||
),
|
||||
("proxyAccountName".to_string(), json!(meta.proxy_account_id)),
|
||||
(
|
||||
"themeColor".to_string(),
|
||||
json!(meta.theme_color.unwrap_or("#31748f".to_string())),
|
||||
),
|
||||
]);
|
||||
|
||||
Ok(Nodeinfo21 {
|
||||
version: "2.1".to_string(),
|
||||
software: Software21 {
|
||||
name: "firefish".to_string(),
|
||||
version: CONFIG.version.clone(),
|
||||
repository: Some(meta.repository_url),
|
||||
homepage: Some("https://firefish.dev/firefish/firefish".to_string()),
|
||||
},
|
||||
protocols: vec![Protocol::Activitypub],
|
||||
services: Services {
|
||||
inbound: vec![],
|
||||
outbound: vec![Outbound::Atom1, Outbound::Rss2],
|
||||
},
|
||||
open_registrations: !meta.disable_registration,
|
||||
usage: Usage {
|
||||
users: Users {
|
||||
total: Some(local_users as u32),
|
||||
active_halfyear: Some(local_active_halfyear as u32),
|
||||
active_month: Some(local_active_month as u32),
|
||||
},
|
||||
local_posts: Some(local_posts as u32),
|
||||
local_comments: None,
|
||||
},
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
|
||||
const NODEINFO_2_1_CACHE_KEY: &str = "nodeinfo_2_1";
|
||||
|
||||
let cached = cache::get::<Nodeinfo21>(NODEINFO_2_1_CACHE_KEY).await?;
|
||||
|
||||
if let Some(nodeinfo) = cached {
|
||||
Ok(nodeinfo)
|
||||
} else {
|
||||
let nodeinfo = generate_nodeinfo_2_1().await?;
|
||||
cache::set(NODEINFO_2_1_CACHE_KEY, &nodeinfo, 60 * 60).await?;
|
||||
Ok(nodeinfo)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn nodeinfo_2_0() -> Result<Nodeinfo20, Error> {
|
||||
Ok(nodeinfo_2_1().await?.into())
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "nodeinfo_2_1")]
|
||||
pub async fn nodeinfo_2_1_as_json() -> Result<serde_json::Value, Error> {
|
||||
Ok(serde_json::to_value(nodeinfo_2_1().await?)?)
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "nodeinfo_2_0")]
|
||||
pub async fn nodeinfo_2_0_as_json() -> Result<serde_json::Value, Error> {
|
||||
Ok(serde_json::to_value(nodeinfo_2_0().await?)?)
|
||||
}
|
3
packages/backend-rs/src/service/nodeinfo/mod.rs
Normal file
3
packages/backend-rs/src/service/nodeinfo/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod fetch;
|
||||
pub mod generate;
|
||||
pub mod schema;
|
263
packages/backend-rs/src/service/nodeinfo/schema.rs
Normal file
263
packages/backend-rs/src/service/nodeinfo/schema.rs
Normal file
|
@ -0,0 +1,263 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// TODO: I want to use these macros but they don't work with rmp_serde
|
||||
// - #[serde(skip_serializing_if = "Option::is_none")] (https://github.com/3Hren/msgpack-rust/issues/86)
|
||||
// - #[serde(tag = "version", rename = "2.1")] (https://github.com/3Hren/msgpack-rust/issues/318)
|
||||
|
||||
/// NodeInfo schema version 2.1. https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.1
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Nodeinfo21 {
|
||||
/// The schema version, must be 2.1.
|
||||
pub version: String,
|
||||
/// Metadata about server software in use.
|
||||
pub software: Software21,
|
||||
/// The protocols supported on this server.
|
||||
pub protocols: Vec<Protocol>,
|
||||
/// The third party sites this server can connect to via their application API.
|
||||
pub services: Services,
|
||||
/// Whether this server allows open self-registration.
|
||||
pub open_registrations: bool,
|
||||
/// Usage statistics for this server.
|
||||
pub usage: Usage,
|
||||
/// Free form key value pairs for software specific values. Clients should not rely on any specific key present.
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// NodeInfo schema version 2.0. https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.0
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object, js_name = "Nodeinfo")]
|
||||
pub struct Nodeinfo20 {
|
||||
/// The schema version, must be 2.0.
|
||||
pub version: String,
|
||||
/// Metadata about server software in use.
|
||||
pub software: Software20,
|
||||
/// The protocols supported on this server.
|
||||
pub protocols: Vec<Protocol>,
|
||||
/// The third party sites this server can connect to via their application API.
|
||||
pub services: Services,
|
||||
/// Whether this server allows open self-registration.
|
||||
pub open_registrations: bool,
|
||||
/// Usage statistics for this server.
|
||||
pub usage: Usage,
|
||||
/// Free form key value pairs for software specific values. Clients should not rely on any specific key present.
|
||||
pub metadata: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Metadata about server software in use (version 2.1).
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Software21 {
|
||||
/// The canonical name of this server software.
|
||||
pub name: String,
|
||||
/// The version of this server software.
|
||||
pub version: String,
|
||||
/// The url of the source code repository of this server software.
|
||||
pub repository: Option<String>,
|
||||
/// The url of the homepage of this server software.
|
||||
pub homepage: Option<String>,
|
||||
}
|
||||
|
||||
/// Metadata about server software in use (version 2.0).
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Software20 {
|
||||
/// The canonical name of this server software.
|
||||
pub name: String,
|
||||
/// The version of this server software.
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[crate::export(string_enum = "lowercase")]
|
||||
pub enum Protocol {
|
||||
Activitypub,
|
||||
Buddycloud,
|
||||
Dfrn,
|
||||
Diaspora,
|
||||
Libertree,
|
||||
Ostatus,
|
||||
Pumpio,
|
||||
Tent,
|
||||
Xmpp,
|
||||
Zot,
|
||||
}
|
||||
|
||||
/// The third party sites this server can connect to via their application API.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Services {
|
||||
/// The third party sites this server can retrieve messages from for combined display with regular traffic.
|
||||
pub inbound: Vec<Inbound>,
|
||||
/// The third party sites this server can publish messages to on the behalf of a user.
|
||||
pub outbound: Vec<Outbound>,
|
||||
}
|
||||
|
||||
/// The third party sites this server can retrieve messages from for combined display with regular traffic.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[crate::export(string_enum = "lowercase")]
|
||||
pub enum Inbound {
|
||||
#[serde(rename = "atom1.0")]
|
||||
Atom1,
|
||||
Gnusocial,
|
||||
Imap,
|
||||
Pnut,
|
||||
#[serde(rename = "pop3")]
|
||||
Pop3,
|
||||
Pumpio,
|
||||
#[serde(rename = "rss2.0")]
|
||||
Rss2,
|
||||
Twitter,
|
||||
}
|
||||
|
||||
/// The third party sites this server can publish messages to on the behalf of a user.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[crate::export(string_enum = "lowercase")]
|
||||
pub enum Outbound {
|
||||
#[serde(rename = "atom1.0")]
|
||||
Atom1,
|
||||
Blogger,
|
||||
Buddycloud,
|
||||
Diaspora,
|
||||
Dreamwidth,
|
||||
Drupal,
|
||||
Facebook,
|
||||
Friendica,
|
||||
Gnusocial,
|
||||
Google,
|
||||
Insanejournal,
|
||||
Libertree,
|
||||
Linkedin,
|
||||
Livejournal,
|
||||
Mediagoblin,
|
||||
Myspace,
|
||||
Pinterest,
|
||||
Pnut,
|
||||
Posterous,
|
||||
Pumpio,
|
||||
Redmatrix,
|
||||
#[serde(rename = "rss2.0")]
|
||||
Rss2,
|
||||
Smtp,
|
||||
Tent,
|
||||
Tumblr,
|
||||
Twitter,
|
||||
Wordpress,
|
||||
Xmpp,
|
||||
}
|
||||
|
||||
/// Usage statistics for this server.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Usage {
|
||||
pub users: Users,
|
||||
pub local_posts: Option<u32>,
|
||||
pub local_comments: Option<u32>,
|
||||
}
|
||||
|
||||
/// statistics about the users of this server.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct Users {
|
||||
pub total: Option<u32>,
|
||||
pub active_halfyear: Option<u32>,
|
||||
pub active_month: Option<u32>,
|
||||
}
|
||||
|
||||
impl From<Software21> for Software20 {
|
||||
fn from(software: Software21) -> Self {
|
||||
Self {
|
||||
name: software.name,
|
||||
version: software.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nodeinfo21> for Nodeinfo20 {
|
||||
fn from(nodeinfo: Nodeinfo21) -> Self {
|
||||
Self {
|
||||
version: "2.0".to_string(),
|
||||
software: nodeinfo.software.into(),
|
||||
protocols: nodeinfo.protocols,
|
||||
services: nodeinfo.services,
|
||||
open_registrations: nodeinfo.open_registrations,
|
||||
usage: nodeinfo.usage,
|
||||
metadata: nodeinfo.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use super::{Nodeinfo20, Nodeinfo21};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_nodeinfo_2_0() {
|
||||
let json_str_1 = r#"{"version":"2.0","software":{"name":"mastodon","version":"4.3.0-nightly.2024-04-30"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":1935016,"activeMonth":238223,"activeHalfyear":618795},"localPosts":90175135},"openRegistrations":true,"metadata":{"nodeName":"Mastodon","nodeDescription":"The original server operated by the Mastodon gGmbH non-profit"}}"#;
|
||||
let parsed_1: Nodeinfo20 = serde_json::from_str(json_str_1).unwrap();
|
||||
let serialized_1 = serde_json::to_string(&parsed_1).unwrap();
|
||||
let reparsed_1: Nodeinfo20 = serde_json::from_str(&serialized_1).unwrap();
|
||||
|
||||
assert_eq!(parsed_1, reparsed_1);
|
||||
assert_eq!(parsed_1.software.name, "mastodon");
|
||||
assert_eq!(parsed_1.software.version, "4.3.0-nightly.2024-04-30");
|
||||
|
||||
let json_str_2 = r#"{"version":"2.0","software":{"name":"peertube","version":"5.0.0"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"openRegistrations":false,"usage":{"users":{"total":5,"activeMonth":0,"activeHalfyear":2},"localPosts":1018,"localComments":1},"metadata":{"taxonomy":{"postsName":"Videos"},"nodeName":"Blender Video","nodeDescription":"Blender Foundation PeerTube instance.","nodeConfig":{"search":{"remoteUri":{"users":true,"anonymous":false}},"plugin":{"registered":[]},"theme":{"registered":[],"default":"default"},"email":{"enabled":false},"contactForm":{"enabled":true},"transcoding":{"hls":{"enabled":true},"webtorrent":{"enabled":true},"enabledResolutions":[1080]},"live":{"enabled":false,"transcoding":{"enabled":true,"enabledResolutions":[]}},"import":{"videos":{"http":{"enabled":true},"torrent":{"enabled":false}}},"autoBlacklist":{"videos":{"ofUsers":{"enabled":false}}},"avatar":{"file":{"size":{"max":4194304},"extensions":[".png",".jpeg",".jpg",".gif",".webp"]}},"video":{"image":{"extensions":[".png",".jpg",".jpeg",".webp"],"size":{"max":4194304}},"file":{"extensions":[".webm",".ogv",".ogg",".mp4",".mkv",".mov",".qt",".mqv",".m4v",".flv",".f4v",".wmv",".avi",".3gp",".3gpp",".3g2",".3gpp2",".nut",".mts",".m2ts",".mpv",".m2v",".m1v",".mpg",".mpe",".mpeg",".vob",".mxf",".mp3",".wma",".wav",".flac",".aac",".m4a",".ac3"]}},"videoCaption":{"file":{"size":{"max":20971520},"extensions":[".vtt",".srt"]}},"user":{"videoQuota":5368709120,"videoQuotaDaily":-1},"trending":{"videos":{"intervalDays":7}},"tracker":{"enabled":true}}}}"#;
|
||||
let parsed_2: Nodeinfo20 = serde_json::from_str(json_str_2).unwrap();
|
||||
let serialized_2 = serde_json::to_string(&parsed_2).unwrap();
|
||||
let reparsed_2: Nodeinfo20 = serde_json::from_str(&serialized_2).unwrap();
|
||||
|
||||
assert_eq!(parsed_2, reparsed_2);
|
||||
assert_eq!(parsed_2.software.name, "peertube");
|
||||
assert_eq!(parsed_2.software.version, "5.0.0");
|
||||
|
||||
let json_str_3 = r#"{"metadata":{"nodeName":"pixelfed","software":{"homepage":"https://pixelfed.org","repo":"https://github.com/pixelfed/pixelfed"},"config":{"features":{"timelines":{"local":true,"network":true},"mobile_apis":true,"stories":true,"video":true,"import":{"instagram":false,"mastodon":false,"pixelfed":false},"label":{"covid":{"enabled":false,"org":"visit the WHO website","url":"https://www.who.int/emergencies/diseases/novel-coronavirus-2019/advice-for-public"}},"hls":{"enabled":false}}}},"protocols":["activitypub"],"services":{"inbound":[],"outbound":[]},"software":{"name":"pixelfed","version":"0.12.0"},"usage":{"localPosts":24059868,"localComments":0,"users":{"total":112832,"activeHalfyear":24366,"activeMonth":8921}},"version":"2.0","openRegistrations":true}"#;
|
||||
let parsed_3: Nodeinfo20 = serde_json::from_str(json_str_3).unwrap();
|
||||
let serialized_3 = serde_json::to_string(&parsed_3).unwrap();
|
||||
let reparsed_3: Nodeinfo20 = serde_json::from_str(&serialized_3).unwrap();
|
||||
|
||||
assert_eq!(parsed_3, reparsed_3);
|
||||
assert_eq!(parsed_3.software.name, "pixelfed");
|
||||
assert_eq!(parsed_3.software.version, "0.12.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_nodeinfo_2_1() {
|
||||
let json_str_1 = r##"{"version":"2.1","software":{"name":"catodon","version":"24.04-dev.2","repository":"https://codeberg.org/catodon/catodon","homepage":"https://codeberg.org/catodon/catodon"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"openRegistrations":true,"usage":{"users":{"total":294,"activeHalfyear":292,"activeMonth":139},"localPosts":22616,"localComments":0},"metadata":{"nodeName":"Catodon Social","nodeDescription":"🌎 Home of Catodon, a new platform for fedi communities, initially based on Iceshrimp/Firefish/Misskey. Be aware that our first release is not out yet, so things are still experimental.","maintainer":{"name":"admin","email":"redacted@example.com"},"langs":[],"tosUrl":"https://example.com/redacted","repositoryUrl":"https://codeberg.org/catodon/catodon","feedbackUrl":"https://codeberg.org/catodon/catodon/issues","disableRegistration":false,"disableLocalTimeline":false,"disableRecommendedTimeline":true,"disableGlobalTimeline":false,"emailRequiredForSignup":true,"postEditing":true,"postImports":false,"enableHcaptcha":true,"enableRecaptcha":false,"maxNoteTextLength":8000,"maxCaptionTextLength":1500,"enableGithubIntegration":false,"enableDiscordIntegration":false,"enableEmail":true,"themeColor":"#31748f"}}"##;
|
||||
let parsed_1: Nodeinfo21 = serde_json::from_str(json_str_1).unwrap();
|
||||
let serialized_1 = serde_json::to_string(&parsed_1).unwrap();
|
||||
let reparsed_1: Nodeinfo21 = serde_json::from_str(&serialized_1).unwrap();
|
||||
|
||||
assert_eq!(parsed_1, reparsed_1);
|
||||
assert_eq!(parsed_1.software.name, "catodon");
|
||||
assert_eq!(parsed_1.software.version, "24.04-dev.2");
|
||||
|
||||
let json_str_2 = r#"{"version":"2.1","software":{"name":"meisskey","version":"10.102.699-m544","repository":"https://github.com/mei23/misskey"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"openRegistrations":true,"usage":{"users":{"total":1123,"activeHalfyear":305,"activeMonth":89},"localPosts":268739,"localComments":0},"metadata":{"nodeName":"meisskey.one","nodeDescription":"ローカルタイムラインのないインスタンスなのだわ\n\n\n[通報・報告 (Report)](https://example.com/redacted)","name":"meisskey.one","description":"ローカルタイムラインのないインスタンスなのだわ\n\n\n[通報・報告 (Report)](https://example.com/redacted)","maintainer":{"name":"redacted","email":"redacted"},"langs":[],"announcements":[{"title":"問題・要望など","text":"問題・要望などは <a href=\"https://example.com/redacted\">#meisskeyone要望</a> で投稿してなのだわ"}],"relayActor":"https://example.com/redacted","relays":[],"disableRegistration":false,"disableLocalTimeline":true,"enableRecaptcha":true,"maxNoteTextLength":5000,"enableTwitterIntegration":false,"enableGithubIntegration":false,"enableDiscordIntegration":false,"enableServiceWorker":true,"proxyAccountName":"ghost"}}"#;
|
||||
let parsed_2: Nodeinfo21 = serde_json::from_str(json_str_2).unwrap();
|
||||
let serialized_2 = serde_json::to_string(&parsed_2).unwrap();
|
||||
let reparsed_2: Nodeinfo21 = serde_json::from_str(&serialized_2).unwrap();
|
||||
|
||||
assert_eq!(parsed_2, reparsed_2);
|
||||
assert_eq!(parsed_2.software.name, "meisskey");
|
||||
assert_eq!(parsed_2.software.version, "10.102.699-m544");
|
||||
|
||||
let json_str_3 = r##"{"metadata":{"enableGlobalTimeline":true,"enableGuestTimeline":false,"enableLocalTimeline":true,"enableRecommendedTimeline":false,"maintainer":{"name":"Firefish dev team"},"nodeDescription":"","nodeName":"Firefish","repositoryUrl":"https://firefish.dev/firefish/firefish","themeColor":"#F25A85"},"openRegistrations":false,"protocols":["activitypub"],"services":{"inbound":[],"outbound":["atom1.0","rss2.0"]},"software":{"homepage":"https://firefish.dev/firefish/firefish","name":"firefish","repository":"https://firefish.dev/firefish/firefish","version":"20240504"},"usage":{"localPosts":23857,"users":{"activeHalfyear":7,"activeMonth":7,"total":9}},"version":"2.1"}"##;
|
||||
let parsed_3: Nodeinfo20 = serde_json::from_str(json_str_3).unwrap();
|
||||
let serialized_3 = serde_json::to_string(&parsed_3).unwrap();
|
||||
let reparsed_3: Nodeinfo20 = serde_json::from_str(&serialized_3).unwrap();
|
||||
|
||||
assert_eq!(parsed_3, reparsed_3);
|
||||
assert_eq!(parsed_3.software.name, "firefish");
|
||||
assert_eq!(parsed_3.software.version, "20240504");
|
||||
}
|
||||
}
|
232
packages/backend-rs/src/service/push_notification.rs
Normal file
232
packages/backend-rs/src/service/push_notification.rs
Normal file
|
@ -0,0 +1,232 @@
|
|||
use crate::database::db_conn;
|
||||
use crate::misc::get_note_summary::{get_note_summary, NoteLike};
|
||||
use crate::misc::meta::fetch_meta;
|
||||
use crate::model::entity::sw_subscription;
|
||||
use crate::util::http_client;
|
||||
use once_cell::sync::OnceCell;
|
||||
use sea_orm::{prelude::*, DbErr};
|
||||
use web_push::{
|
||||
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
|
||||
WebPushClient, WebPushError, WebPushMessageBuilder,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
DbErr(#[from] DbErr),
|
||||
#[error("Web Push error: {0}")]
|
||||
WebPushErr(#[from] WebPushError),
|
||||
#[error("Failed to (de)serialize an object: {0}")]
|
||||
SerializeErr(#[from] serde_json::Error),
|
||||
#[error("Invalid content: {0}")]
|
||||
InvalidContentErr(String),
|
||||
#[error("HTTP client aquisition error: {0}")]
|
||||
HttpClientErr(#[from] http_client::Error),
|
||||
}
|
||||
|
||||
static CLIENT: OnceCell<IsahcWebPushClient> = OnceCell::new();
|
||||
|
||||
fn get_client() -> Result<IsahcWebPushClient, Error> {
|
||||
Ok(CLIENT
|
||||
.get_or_try_init(|| http_client::client().map(IsahcWebPushClient::from))
|
||||
.cloned()?)
|
||||
}
|
||||
|
||||
#[derive(strum::Display, PartialEq)]
|
||||
#[crate::export(string_enum = "camelCase")]
|
||||
pub enum PushNotificationKind {
|
||||
#[strum(serialize = "notification")]
|
||||
Generic,
|
||||
#[strum(serialize = "unreadMessagingMessage")]
|
||||
Chat,
|
||||
#[strum(serialize = "readAllMessagingMessages")]
|
||||
ReadAllChats,
|
||||
#[strum(serialize = "readAllMessagingMessagesOfARoom")]
|
||||
ReadAllChatsInTheRoom,
|
||||
#[strum(serialize = "readNotifications")]
|
||||
ReadNotifications,
|
||||
#[strum(serialize = "readAllNotifications")]
|
||||
ReadAllNotifications,
|
||||
}
|
||||
|
||||
fn compact_content(
|
||||
kind: &PushNotificationKind,
|
||||
mut content: serde_json::Value,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
if kind != &PushNotificationKind::Generic {
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
if !content.is_object() {
|
||||
return Err(Error::InvalidContentErr("not a JSON object".to_string()));
|
||||
}
|
||||
|
||||
let object = content.as_object_mut().unwrap();
|
||||
|
||||
if !object.contains_key("note") {
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
let mut note = if object.contains_key("type") && object.get("type").unwrap() == "renote" {
|
||||
object
|
||||
.get("note")
|
||||
.unwrap()
|
||||
.get("renote")
|
||||
.ok_or(Error::InvalidContentErr(
|
||||
"renote object is missing".to_string(),
|
||||
))?
|
||||
} else {
|
||||
object.get("note").unwrap()
|
||||
}
|
||||
.clone();
|
||||
|
||||
if !note.is_object() {
|
||||
return Err(Error::InvalidContentErr(
|
||||
"(re)note is not an object".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let note_like: NoteLike = serde_json::from_value(note.clone())?;
|
||||
let text = get_note_summary(note_like);
|
||||
|
||||
let note_object = note.as_object_mut().unwrap();
|
||||
|
||||
note_object.remove("reply");
|
||||
note_object.remove("renote");
|
||||
note_object.remove("user");
|
||||
note_object.insert("text".to_string(), text.into());
|
||||
object.insert("note".to_string(), note);
|
||||
|
||||
Ok(serde_json::from_value(Json::Object(object.clone()))?)
|
||||
}
|
||||
|
||||
async fn handle_web_push_failure(
|
||||
db: &DatabaseConnection,
|
||||
err: WebPushError,
|
||||
subscription_id: &str,
|
||||
error_message: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
match err {
|
||||
WebPushError::BadRequest(_)
|
||||
| WebPushError::ServerError(_)
|
||||
| WebPushError::InvalidUri
|
||||
| WebPushError::EndpointNotValid
|
||||
| WebPushError::EndpointNotFound
|
||||
| WebPushError::TlsError
|
||||
| WebPushError::SslError
|
||||
| WebPushError::InvalidPackageName
|
||||
| WebPushError::MissingCryptoKeys
|
||||
| WebPushError::InvalidCryptoKeys
|
||||
| WebPushError::InvalidResponse => {
|
||||
sw_subscription::Entity::delete_by_id(subscription_id)
|
||||
.exec(db)
|
||||
.await?;
|
||||
tracing::info!("{}; {} was unsubscribed", error_message, subscription_id);
|
||||
tracing::debug!("reason: {:#?}", err);
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("{}; subscription id: {}", error_message, subscription_id);
|
||||
tracing::info!("reason: {:#?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[crate::export]
|
||||
pub async fn send_push_notification(
|
||||
receiver_user_id: &str,
|
||||
kind: PushNotificationKind,
|
||||
content: &serde_json::Value,
|
||||
) -> Result<(), Error> {
|
||||
let meta = fetch_meta(true).await?;
|
||||
|
||||
if !meta.enable_service_worker || meta.sw_public_key.is_none() || meta.sw_private_key.is_none()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db = db_conn().await?;
|
||||
|
||||
let signature_builder = VapidSignatureBuilder::from_base64_no_sub(
|
||||
meta.sw_private_key.unwrap().as_str(),
|
||||
web_push::URL_SAFE_NO_PAD,
|
||||
)?;
|
||||
|
||||
let subscriptions = sw_subscription::Entity::find()
|
||||
.filter(sw_subscription::Column::UserId.eq(receiver_user_id))
|
||||
.all(db)
|
||||
.await?;
|
||||
|
||||
let payload = format!(
|
||||
"{{\"type\":\"{}\",\"userId\":\"{}\",\"dateTime\":{},\"body\":{}}}",
|
||||
kind,
|
||||
receiver_user_id,
|
||||
chrono::Utc::now().timestamp_millis(),
|
||||
serde_json::to_string(&compact_content(&kind, content.clone())?)?
|
||||
);
|
||||
tracing::trace!("payload: {:#?}", payload);
|
||||
|
||||
for subscription in subscriptions.iter() {
|
||||
if !subscription.send_read_message
|
||||
&& [
|
||||
PushNotificationKind::ReadAllChats,
|
||||
PushNotificationKind::ReadAllChatsInTheRoom,
|
||||
PushNotificationKind::ReadAllNotifications,
|
||||
PushNotificationKind::ReadNotifications,
|
||||
]
|
||||
.contains(&kind)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let subscription_info = SubscriptionInfo {
|
||||
endpoint: subscription.endpoint.to_owned(),
|
||||
keys: SubscriptionKeys {
|
||||
// convert standard base64 into base64url
|
||||
// https://en.wikipedia.org/wiki/Base64#Variants_summary_table
|
||||
p256dh: subscription
|
||||
.publickey
|
||||
.replace('+', "-")
|
||||
.replace('/', "_")
|
||||
.to_owned(),
|
||||
auth: subscription
|
||||
.auth
|
||||
.replace('+', "-")
|
||||
.replace('/', "_")
|
||||
.to_owned(),
|
||||
},
|
||||
};
|
||||
|
||||
let signature = signature_builder
|
||||
.clone()
|
||||
.add_sub_info(&subscription_info)
|
||||
.build();
|
||||
|
||||
if let Err(err) = signature {
|
||||
handle_web_push_failure(db, err, &subscription.id, "failed to build a signature")
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut message_builder = WebPushMessageBuilder::new(&subscription_info);
|
||||
message_builder.set_ttl(1000);
|
||||
message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes());
|
||||
message_builder.set_vapid_signature(signature.unwrap());
|
||||
|
||||
let message = message_builder.build();
|
||||
|
||||
if let Err(err) = message {
|
||||
handle_web_push_failure(db, err, &subscription.id, "failed to build a payload").await?;
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = get_client()?.send(message.unwrap()).await {
|
||||
handle_web_push_failure(db, err, &subscription.id, "failed to send").await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::debug!("success; subscription id: {}", subscription.id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,18 +1,23 @@
|
|||
pub mod antenna;
|
||||
pub mod channel;
|
||||
pub mod chat;
|
||||
pub mod chat_index;
|
||||
pub mod custom_emoji;
|
||||
pub mod group_chat;
|
||||
pub mod moderation;
|
||||
|
||||
use crate::config::CONFIG;
|
||||
use crate::database::redis_conn;
|
||||
use redis::{Commands, RedisError};
|
||||
use crate::database::{redis_conn, RedisConnError};
|
||||
use redis::{AsyncCommands, RedisError};
|
||||
|
||||
#[derive(strum::Display)]
|
||||
pub enum Stream {
|
||||
#[strum(serialize = "internal")]
|
||||
Internal,
|
||||
#[strum(serialize = "broadcast")]
|
||||
Broadcast,
|
||||
#[strum(to_string = "adminStream:{user_id}")]
|
||||
Admin { user_id: String },
|
||||
CustomEmoji,
|
||||
#[strum(to_string = "adminStream:{moderator_id}")]
|
||||
Moderation { moderator_id: String },
|
||||
#[strum(to_string = "user:{user_id}")]
|
||||
User { user_id: String },
|
||||
#[strum(to_string = "channelStream:{channel_id}")]
|
||||
|
@ -37,27 +42,29 @@ pub enum Stream {
|
|||
#[strum(to_string = "messagingStream:{group_id}")]
|
||||
GroupChat { group_id: String },
|
||||
#[strum(to_string = "messagingIndexStream:{user_id}")]
|
||||
MessagingIndex { user_id: String },
|
||||
ChatIndex { user_id: String },
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Redis error: {0}")]
|
||||
RedisError(#[from] RedisError),
|
||||
#[error("Redis connection error: {0}")]
|
||||
RedisConnErr(#[from] RedisConnError),
|
||||
#[error("Json (de)serialization error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
#[error("Value error: {0}")]
|
||||
ValueError(String),
|
||||
}
|
||||
|
||||
pub fn publish_to_stream(
|
||||
pub async fn publish_to_stream(
|
||||
stream: &Stream,
|
||||
kind: Option<String>,
|
||||
value: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let message = if let Some(kind) = kind {
|
||||
format!(
|
||||
"{{ \"type\": \"{}\", \"body\": {} }}",
|
||||
"{{\"type\":\"{}\",\"body\":{}}}",
|
||||
kind,
|
||||
value.unwrap_or("null".to_string()),
|
||||
)
|
||||
|
@ -65,13 +72,13 @@ pub fn publish_to_stream(
|
|||
value.ok_or(Error::ValueError("Invalid streaming message".to_string()))?
|
||||
};
|
||||
|
||||
redis_conn()?.publish(
|
||||
&CONFIG.host,
|
||||
format!(
|
||||
"{{ \"channel\": \"{}\", \"message\": {} }}",
|
||||
stream, message,
|
||||
),
|
||||
)?;
|
||||
redis_conn()
|
||||
.await?
|
||||
.publish(
|
||||
&CONFIG.host,
|
||||
format!("{{\"channel\":\"{}\",\"message\":{}}}", stream, message),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -84,10 +91,10 @@ mod unit_test {
|
|||
#[test]
|
||||
fn channel_to_string() {
|
||||
assert_eq!(Stream::Internal.to_string(), "internal");
|
||||
assert_eq!(Stream::Broadcast.to_string(), "broadcast");
|
||||
assert_eq!(Stream::CustomEmoji.to_string(), "broadcast");
|
||||
assert_eq!(
|
||||
Stream::Admin {
|
||||
user_id: "9tb42br63g5apjcq".to_string()
|
||||
Stream::Moderation {
|
||||
moderator_id: "9tb42br63g5apjcq".to_string()
|
||||
}
|
||||
.to_string(),
|
||||
"adminStream:9tb42br63g5apjcq"
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::model::entity::note;
|
||||
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||
|
||||
pub fn publish(antenna_id: String, note: ¬e::Model) -> Result<(), Error> {
|
||||
pub async fn publish(antenna_id: String, note: ¬e::Model) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::Antenna { antenna_id },
|
||||
Some("note".to_string()),
|
||||
Some(serde_json::to_string(note)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
11
packages/backend-rs/src/service/stream/channel.rs
Normal file
11
packages/backend-rs/src/service/stream/channel.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||
|
||||
#[crate::export(js_name = "publishToChannelStream")]
|
||||
pub async fn publish(channel_id: String, user_id: String) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::Channel { channel_id },
|
||||
Some("typing".to_string()),
|
||||
Some(format!("\"{}\"", user_id)),
|
||||
)
|
||||
.await
|
||||
}
|
|
@ -13,12 +13,15 @@ pub enum ChatEvent {
|
|||
Typing,
|
||||
}
|
||||
|
||||
// We want to merge `kind` and `object` into a single enum
|
||||
// https://github.com/napi-rs/napi-rs/issues/2036
|
||||
|
||||
#[crate::export(js_name = "publishToChatStream")]
|
||||
pub fn publish(
|
||||
pub async fn publish(
|
||||
sender_user_id: String,
|
||||
receiver_user_id: String,
|
||||
kind: ChatEvent,
|
||||
object: &serde_json::Value, // TODO?: change this to enum
|
||||
object: &serde_json::Value,
|
||||
) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::Chat {
|
||||
|
@ -28,4 +31,5 @@ pub fn publish(
|
|||
Some(kind.to_string()),
|
||||
Some(serde_json::to_string(object)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
27
packages/backend-rs/src/service/stream/chat_index.rs
Normal file
27
packages/backend-rs/src/service/stream/chat_index.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||
|
||||
#[derive(strum::Display)]
|
||||
#[crate::export(string_enum = "camelCase")]
|
||||
pub enum ChatIndexEvent {
|
||||
#[strum(serialize = "message")]
|
||||
Message,
|
||||
#[strum(serialize = "read")]
|
||||
Read,
|
||||
}
|
||||
|
||||
// We want to merge `kind` and `object` into a single enum
|
||||
// https://github.com/napi-rs/napi-rs/issues/2036
|
||||
|
||||
#[crate::export(js_name = "publishToChatIndexStream")]
|
||||
pub async fn publish(
|
||||
user_id: String,
|
||||
kind: ChatIndexEvent,
|
||||
object: &serde_json::Value,
|
||||
) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::ChatIndex { user_id },
|
||||
Some(kind.to_string()),
|
||||
Some(serde_json::to_string(object)?),
|
||||
)
|
||||
.await
|
||||
}
|
28
packages/backend-rs/src/service/stream/custom_emoji.rs
Normal file
28
packages/backend-rs/src/service/stream/custom_emoji.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// TODO: define schema type in other place
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct PackedEmoji {
|
||||
pub id: String,
|
||||
pub aliases: Vec<String>,
|
||||
pub name: String,
|
||||
pub category: Option<String>,
|
||||
pub host: Option<String>,
|
||||
pub url: String,
|
||||
pub license: Option<String>,
|
||||
pub width: Option<i32>,
|
||||
pub height: Option<i32>,
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "publishToBroadcastStream")]
|
||||
pub async fn publish(emoji: &PackedEmoji) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::CustomEmoji,
|
||||
Some("emojiAdded".to_string()),
|
||||
Some(format!("{{\"emoji\":{}}}", serde_json::to_string(emoji)?)),
|
||||
)
|
||||
.await
|
||||
}
|
18
packages/backend-rs/src/service/stream/group_chat.rs
Normal file
18
packages/backend-rs/src/service/stream/group_chat.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use crate::service::stream::{chat::ChatEvent, publish_to_stream, Error, Stream};
|
||||
|
||||
// We want to merge `kind` and `object` into a single enum
|
||||
// https://github.com/napi-rs/napi-rs/issues/2036
|
||||
|
||||
#[crate::export(js_name = "publishToGroupChatStream")]
|
||||
pub async fn publish(
|
||||
group_id: String,
|
||||
kind: ChatEvent,
|
||||
object: &serde_json::Value,
|
||||
) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::GroupChat { group_id },
|
||||
Some(kind.to_string()),
|
||||
Some(serde_json::to_string(object)?),
|
||||
)
|
||||
.await
|
||||
}
|
22
packages/backend-rs/src/service/stream/moderation.rs
Normal file
22
packages/backend-rs/src/service/stream/moderation.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[crate::export(object)]
|
||||
pub struct AbuseUserReportLike {
|
||||
pub id: String,
|
||||
pub target_user_id: String,
|
||||
pub reporter_id: String,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "publishToModerationStream")]
|
||||
pub async fn publish(moderator_id: String, report: &AbuseUserReportLike) -> Result<(), Error> {
|
||||
publish_to_stream(
|
||||
&Stream::Moderation { moderator_id },
|
||||
Some("newAbuseUserReport".to_string()),
|
||||
Some(serde_json::to_string(report)?),
|
||||
)
|
||||
.await
|
||||
}
|
34
packages/backend-rs/src/util/http_client.rs
Normal file
34
packages/backend-rs/src/util/http_client.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use crate::config::CONFIG;
|
||||
use isahc::{config::*, HttpClient};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Isahc error: {0}")]
|
||||
IsahcErr(#[from] isahc::Error),
|
||||
#[error("Url parse error: {0}")]
|
||||
UrlParseErr(#[from] isahc::http::uri::InvalidUri),
|
||||
}
|
||||
|
||||
static CLIENT: OnceCell<HttpClient> = OnceCell::new();
|
||||
|
||||
pub fn client() -> Result<HttpClient, Error> {
|
||||
CLIENT
|
||||
.get_or_try_init(|| {
|
||||
let mut builder = HttpClient::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.default_header("user-agent", &CONFIG.user_agent)
|
||||
.dns_cache(DnsCache::Timeout(Duration::from_secs(60 * 60)));
|
||||
|
||||
if let Some(proxy_url) = &CONFIG.proxy {
|
||||
builder = builder.proxy(Some(proxy_url.parse()?));
|
||||
if let Some(proxy_bypass_hosts) = &CONFIG.proxy_bypass_hosts {
|
||||
builder = builder.proxy_blacklist(proxy_bypass_hosts);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
})
|
||||
.cloned()
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod http_client;
|
||||
pub mod id;
|
||||
pub mod random;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
/// Generate random string based on [thread_rng] and [Alphanumeric].
|
||||
pub fn gen_string(length: u16) -> String {
|
||||
#[crate::export]
|
||||
pub fn generate_secure_random_string(length: u16) -> String {
|
||||
thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(length.into())
|
||||
|
@ -9,9 +10,9 @@ pub fn gen_string(length: u16) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[crate::export(js_name = "secureRndstr")]
|
||||
pub fn native_random_str(length: Option<u16>) -> String {
|
||||
gen_string(length.unwrap_or(32))
|
||||
#[crate::export]
|
||||
pub fn generate_user_token() -> String {
|
||||
generate_secure_random_string(16)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -19,14 +20,17 @@ mod unit_test {
|
|||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use std::thread;
|
||||
|
||||
use super::gen_string;
|
||||
use super::generate_secure_random_string;
|
||||
|
||||
#[test]
|
||||
fn can_generate_unique_strings() {
|
||||
assert_eq!(gen_string(16).len(), 16);
|
||||
assert_ne!(gen_string(16), gen_string(16));
|
||||
let s1 = thread::spawn(|| gen_string(16));
|
||||
let s2 = thread::spawn(|| gen_string(16));
|
||||
assert_eq!(generate_secure_random_string(16).len(), 16);
|
||||
assert_ne!(
|
||||
generate_secure_random_string(16),
|
||||
generate_secure_random_string(16)
|
||||
);
|
||||
let s1 = thread::spawn(|| generate_secure_random_string(16));
|
||||
let s2 = thread::spawn(|| generate_secure_random_string(16));
|
||||
assert_ne!(s1.join().unwrap(), s2.join().unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,54 +22,54 @@
|
|||
"@swc/core-android-arm64": "1.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.15.5",
|
||||
"@bull-board/koa": "5.15.5",
|
||||
"@bull-board/ui": "5.15.5",
|
||||
"@discordapp/twemoji": "^15.0.3",
|
||||
"@bull-board/api": "5.17.1",
|
||||
"@bull-board/koa": "5.17.1",
|
||||
"@bull-board/ui": "5.17.1",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "12.0.1",
|
||||
"@ladjs/koa-views": "9.0.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@redocly/openapi-core": "1.11.0",
|
||||
"@redocly/openapi-core": "1.12.2",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"adm-zip": "0.5.10",
|
||||
"ajv": "8.12.0",
|
||||
"ajv": "8.13.0",
|
||||
"archiver": "7.0.1",
|
||||
"aws-sdk": "2.1599.0",
|
||||
"axios": "^1.6.8",
|
||||
"aws-sdk": "2.1623.0",
|
||||
"axios": "1.6.8",
|
||||
"backend-rs": "workspace:*",
|
||||
"blurhash": "2.0.5",
|
||||
"bull": "4.12.2",
|
||||
"bull": "4.12.6",
|
||||
"cacheable-lookup": "TheEssem/cacheable-lookup",
|
||||
"cbor-x": "^1.5.9",
|
||||
"cbor-x": "1.5.9",
|
||||
"chalk": "5.3.0",
|
||||
"chalk-template": "1.1.0",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "3.6.0",
|
||||
"decompress": "^4.2.1",
|
||||
"decompress": "4.2.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"deepl-node": "1.13.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"firefish-js": "workspace:*",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.2.1",
|
||||
"gunzip-maybe": "^1.4.2",
|
||||
"happy-dom": "^14.7.1",
|
||||
"gunzip-maybe": "1.4.2",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "5.3.2",
|
||||
"ioredis": "5.4.1",
|
||||
"ip-cidr": "4.0.0",
|
||||
"is-svg": "5.0.0",
|
||||
"is-svg": "5.0.1",
|
||||
"jsdom": "24.0.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "11.1.0",
|
||||
"katex": "0.16.10",
|
||||
"koa": "2.15.3",
|
||||
"koa-body": "^6.0.1",
|
||||
"koa-body": "6.0.1",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
"koa-favicon": "2.1.0",
|
||||
"koa-json-body": "5.3.0",
|
||||
|
@ -81,14 +81,13 @@
|
|||
"megalodon": "workspace:*",
|
||||
"mfm-js": "0.24.0",
|
||||
"mime-types": "2.1.35",
|
||||
"msgpackr": "^1.10.1",
|
||||
"msgpackr": "1.10.2",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.13",
|
||||
"opencc-js": "^1.0.5",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "^9.2.3",
|
||||
"opencc-js": "1.0.5",
|
||||
"otpauth": "9.2.4",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.5",
|
||||
"private-ip": "3.0.2",
|
||||
|
@ -106,33 +105,32 @@
|
|||
"rndstr": "1.0.0",
|
||||
"rss-parser": "3.13.0",
|
||||
"sanitize-html": "2.13.0",
|
||||
"semver": "7.6.0",
|
||||
"sharp": "0.33.3",
|
||||
"semver": "7.6.2",
|
||||
"sharp": "0.33.4",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "1.0.0",
|
||||
"systeminformation": "5.22.7",
|
||||
"tar-stream": "^3.1.7",
|
||||
"tesseract.js": "^5.0.5",
|
||||
"tar-stream": "3.1.7",
|
||||
"tesseract.js": "5.1.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.3",
|
||||
"typeorm": "0.3.20",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "9.0.1",
|
||||
"web-push": "3.6.7",
|
||||
"websocket": "1.0.34",
|
||||
"websocket": "1.0.35",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "0.3.12",
|
||||
"@swc/core": "1.4.13",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/color-convert": "^2.0.3",
|
||||
"@types/content-disposition": "^0.5.8",
|
||||
"@swc/core": "1.5.7",
|
||||
"@types/adm-zip": "0.5.5",
|
||||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/jsdom": "21.1.6",
|
||||
"@types/jsonld": "1.5.13",
|
||||
"@types/jsrsasign": "10.5.13",
|
||||
"@types/jsrsasign": "10.5.14",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/koa": "2.15.0",
|
||||
"@types/koa-bodyparser": "4.3.12",
|
||||
|
@ -145,24 +143,24 @@
|
|||
"@types/koa__multer": "2.0.7",
|
||||
"@types/koa__router": "12.0.4",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/node-fetch": "2.6.11",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/nodemailer": "6.4.15",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/opencc-js": "^1.0.3",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/probe-image-size": "^7.2.4",
|
||||
"@types/opencc-js": "1.0.3",
|
||||
"@types/pg": "8.11.6",
|
||||
"@types/probe-image-size": "7.2.4",
|
||||
"@types/pug": "2.0.10",
|
||||
"@types/punycode": "2.1.4",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/qs": "6.9.14",
|
||||
"@types/qs": "6.9.15",
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@types/ratelimiter": "3.4.6",
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.11.0",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/syslog-pro": "^1.0.3",
|
||||
"@types/syslog-pro": "1.0.3",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/uuid": "9.0.8",
|
||||
|
@ -170,17 +168,17 @@
|
|||
"@types/websocket": "1.0.10",
|
||||
"@types/ws": "8.5.10",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint": "9.3.0",
|
||||
"mocha": "10.4.0",
|
||||
"pug": "3.0.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"swc-loader": "^0.2.6",
|
||||
"swc-loader": "0.2.6",
|
||||
"ts-loader": "9.5.1",
|
||||
"ts-node": "10.9.2",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"type-fest": "4.15.0",
|
||||
"type-fest": "4.18.2",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "^5.91.0",
|
||||
"ws": "8.16.0"
|
||||
"webpack": "5.91.0",
|
||||
"ws": "8.17.0"
|
||||
}
|
||||
}
|
||||
|
|
33
packages/backend/src/@types/os-utils.d.ts
vendored
33
packages/backend/src/@types/os-utils.d.ts
vendored
|
@ -1,33 +0,0 @@
|
|||
declare module "os-utils" {
|
||||
type FreeCommandCallback = (usedmem: number) => void;
|
||||
|
||||
type HarddriveCallback = (total: number, free: number, used: number) => void;
|
||||
|
||||
type GetProcessesCallback = (result: string) => void;
|
||||
|
||||
type CPUCallback = (perc: number) => void;
|
||||
|
||||
export function platform(): NodeJS.Platform;
|
||||
export function cpuCount(): number;
|
||||
export function sysUptime(): number;
|
||||
export function processUptime(): number;
|
||||
|
||||
export function freemem(): number;
|
||||
export function totalmem(): number;
|
||||
export function freememPercentage(): number;
|
||||
export function freeCommand(callback: FreeCommandCallback): void;
|
||||
|
||||
export function harddrive(callback: HarddriveCallback): void;
|
||||
|
||||
export function getProcesses(callback: GetProcessesCallback): void;
|
||||
export function getProcesses(
|
||||
nProcess: number,
|
||||
callback: GetProcessesCallback,
|
||||
): void;
|
||||
|
||||
export function allLoadavg(): string;
|
||||
export function loadavg(_time?: number): number;
|
||||
|
||||
export function cpuFree(callback: CPUCallback): void;
|
||||
export function cpuUsage(callback: CPUCallback): void;
|
||||
}
|
|
@ -1,97 +1,33 @@
|
|||
import * as fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import * as os from "node:os";
|
||||
import cluster from "node:cluster";
|
||||
import chalk from "chalk";
|
||||
import chalkTemplate from "chalk-template";
|
||||
import semver from "semver";
|
||||
|
||||
import Logger from "@/services/logger.js";
|
||||
import type { Config } from "backend-rs";
|
||||
import { fetchMeta, removeOldAttestationChallenges } from "backend-rs";
|
||||
import {
|
||||
fetchMeta,
|
||||
greet,
|
||||
initializeRustLogger,
|
||||
removeOldAttestationChallenges,
|
||||
showServerInfo,
|
||||
type Config,
|
||||
} from "backend-rs";
|
||||
import { config, envOption } from "@/config.js";
|
||||
import { showMachineInfo } from "@/misc/show-machine-info.js";
|
||||
import { db, initDb } from "@/db/postgre.js";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const meta = JSON.parse(
|
||||
fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
|
||||
);
|
||||
|
||||
const logger = new Logger("core", "cyan");
|
||||
const bootLogger = logger.createSubLogger("boot", "magenta", false);
|
||||
|
||||
const themeColor = chalk.hex("#31748f");
|
||||
|
||||
function greet() {
|
||||
//#region Firefish logo
|
||||
console.log(
|
||||
themeColor(
|
||||
"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
themeColor(
|
||||
"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
themeColor(
|
||||
"█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
themeColor(
|
||||
"██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
themeColor(
|
||||
"██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ",
|
||||
),
|
||||
);
|
||||
console.log(
|
||||
themeColor(
|
||||
"╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ",
|
||||
),
|
||||
);
|
||||
//#endregion
|
||||
|
||||
console.log(
|
||||
" Firefish is an open-source decentralized microblogging platform.",
|
||||
);
|
||||
console.log(
|
||||
chalk.rgb(
|
||||
255,
|
||||
136,
|
||||
0,
|
||||
)(
|
||||
" If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish",
|
||||
),
|
||||
);
|
||||
|
||||
console.log("");
|
||||
console.log(
|
||||
chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
|
||||
);
|
||||
|
||||
bootLogger.info("Welcome to Firefish!");
|
||||
bootLogger.info(`Firefish v${meta.version}`, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init master process
|
||||
*/
|
||||
export async function masterMain() {
|
||||
// initialize app
|
||||
try {
|
||||
initializeRustLogger();
|
||||
greet();
|
||||
showEnvironment();
|
||||
await showMachineInfo(bootLogger);
|
||||
showServerInfo();
|
||||
showNodejsVersion();
|
||||
await connectDb();
|
||||
} catch (e) {
|
||||
|
@ -103,13 +39,13 @@ export async function masterMain() {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
bootLogger.succ("Firefish initialized");
|
||||
bootLogger.info("Firefish initialized");
|
||||
|
||||
if (!envOption.disableClustering) {
|
||||
await spawnWorkers(config.clusterLimits);
|
||||
}
|
||||
|
||||
bootLogger.succ(
|
||||
bootLogger.info(
|
||||
`Now listening on port ${config.port} on ${config.url}`,
|
||||
null,
|
||||
true,
|
||||
|
@ -160,7 +96,7 @@ async function connectDb(): Promise<void> {
|
|||
const v = await db
|
||||
.query("SHOW server_version")
|
||||
.then((x) => x[0].server_version);
|
||||
dbLogger.succ(`Connected: v${v}`);
|
||||
dbLogger.info(`Connected: v${v}`);
|
||||
} catch (e) {
|
||||
dbLogger.error("Failed to connect to the database", null, true);
|
||||
dbLogger.error(inspect(e));
|
||||
|
@ -196,7 +132,7 @@ async function spawnWorkers(
|
|||
`Starting ${clusterLimits.web} web workers and ${clusterLimits.queue} queue workers (total ${total})...`,
|
||||
);
|
||||
await Promise.all(workers.map((mode) => spawnWorker(mode)));
|
||||
bootLogger.succ("All workers started");
|
||||
bootLogger.info("All workers started");
|
||||
}
|
||||
|
||||
function spawnWorker(mode: "web" | "queue"): Promise<void> {
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
import { config } from "@/config.js";
|
||||
import {
|
||||
DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||
DB_MAX_NOTE_TEXT_LENGTH,
|
||||
} from "@/misc/hard-limits.js";
|
||||
|
||||
export const MAX_NOTE_TEXT_LENGTH = Math.min(
|
||||
config.maxNoteLength ?? 3000,
|
||||
DB_MAX_NOTE_TEXT_LENGTH,
|
||||
);
|
||||
export const MAX_CAPTION_TEXT_LENGTH = Math.min(
|
||||
config.maxCaptionLength ?? 1500,
|
||||
DB_MAX_IMAGE_COMMENT_LENGTH,
|
||||
);
|
||||
|
||||
export const SECOND = 1000;
|
||||
export const MINUTE = 60 * SECOND;
|
||||
export const HOUR = 60 * MINUTE;
|
||||
export const DAY = 24 * HOUR;
|
||||
|
||||
export const USER_ONLINE_THRESHOLD = 10 * MINUTE;
|
||||
export const USER_ACTIVE_THRESHOLD = 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
|
||||
export const FILE_TYPE_BROWSERSAFE = [
|
||||
// Images
|
||||
"image/png",
|
||||
"image/gif", // TODO: deprecated, but still used by old notes, 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",
|
||||
];
|
||||
/*
|
||||
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
|
||||
*/
|
|
@ -1,15 +1,8 @@
|
|||
import si from "systeminformation";
|
||||
import Xev from "xev";
|
||||
import * as osUtils from "os-utils";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
import { fetchMeta, cpuUsage, memoryUsage } from "backend-rs";
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
const interval = 2000;
|
||||
|
||||
const roundCpu = (num: number) => Math.round(num * 1000) / 1000;
|
||||
const round = (num: number) => Math.round(num * 10) / 10;
|
||||
|
||||
/**
|
||||
* Report server stats regularly
|
||||
*/
|
||||
|
@ -24,26 +17,9 @@ export default async function () {
|
|||
if (!meta.enableServerMachineStats) return;
|
||||
|
||||
async function tick() {
|
||||
const cpu = await cpuUsage();
|
||||
const memStats = await mem();
|
||||
const netStats = await net();
|
||||
const fsStats = await fs();
|
||||
|
||||
const stats = {
|
||||
cpu: roundCpu(cpu),
|
||||
mem: {
|
||||
used: round(memStats.used - memStats.buffers - memStats.cached),
|
||||
active: round(memStats.active),
|
||||
total: round(memStats.total),
|
||||
},
|
||||
net: {
|
||||
rx: round(Math.max(0, netStats.rx_sec)),
|
||||
tx: round(Math.max(0, netStats.tx_sec)),
|
||||
},
|
||||
fs: {
|
||||
r: round(Math.max(0, fsStats.rIO_sec ?? 0)),
|
||||
w: round(Math.max(0, fsStats.wIO_sec ?? 0)),
|
||||
},
|
||||
cpu: cpuUsage(),
|
||||
mem: memoryUsage(),
|
||||
};
|
||||
ev.emit("serverStats", stats);
|
||||
log.unshift(stats);
|
||||
|
@ -52,33 +28,5 @@ export default async function () {
|
|||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
||||
}
|
||||
|
||||
// CPU STAT
|
||||
function cpuUsage(): Promise<number> {
|
||||
return new Promise((res, rej) => {
|
||||
osUtils.cpuUsage((cpuUsage) => {
|
||||
res(cpuUsage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// MEMORY STAT
|
||||
async function mem() {
|
||||
const data = await si.mem();
|
||||
return data;
|
||||
}
|
||||
|
||||
// NETWORK STAT
|
||||
async function net() {
|
||||
const iface = await si.networkInterfaceDefault();
|
||||
const data = await si.networkStats(iface);
|
||||
return data[0];
|
||||
}
|
||||
|
||||
// FS STAT
|
||||
async function fs() {
|
||||
const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
|
||||
return data || { rIO_sec: 0, wIO_sec: 0 };
|
||||
setInterval(tick, 3000);
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ import { dbLogger } from "./logger.js";
|
|||
|
||||
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
||||
|
||||
class MyCustomLogger implements Logger {
|
||||
class DbLogger implements Logger {
|
||||
private highlight(sql: string) {
|
||||
return highlight.highlight(sql, {
|
||||
language: "sql",
|
||||
|
@ -89,15 +89,16 @@ class MyCustomLogger implements Logger {
|
|||
}
|
||||
|
||||
public logQuery(query: string, parameters?: any[]) {
|
||||
sqlLogger.info(this.highlight(query).substring(0, 100));
|
||||
sqlLogger.trace(this.highlight(query).substring(0, 100));
|
||||
}
|
||||
|
||||
public logQueryError(error: string, query: string, parameters?: any[]) {
|
||||
sqlLogger.error(this.highlight(query));
|
||||
sqlLogger.error(error);
|
||||
sqlLogger.trace(this.highlight(query));
|
||||
}
|
||||
|
||||
public logQuerySlow(time: number, query: string, parameters?: any[]) {
|
||||
sqlLogger.warn(this.highlight(query));
|
||||
sqlLogger.trace(this.highlight(query));
|
||||
}
|
||||
|
||||
public logSchemaBuild(message: string) {
|
||||
|
@ -215,7 +216,7 @@ export const db = new DataSource({
|
|||
}
|
||||
: false,
|
||||
logging: log,
|
||||
logger: log ? new MyCustomLogger() : undefined,
|
||||
logger: log ? new DbLogger() : undefined,
|
||||
maxQueryExecutionTime: 300,
|
||||
entities: entities,
|
||||
migrations: ["../../migration/*.js"],
|
||||
|
|
|
@ -19,6 +19,13 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
return appendChildren(childNodes, background).join("").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* We only exclude text containing asterisks, since the other marks can almost be considered intentionally used.
|
||||
*/
|
||||
function escapeAmbiguousMfmMarks(text: string) {
|
||||
return text.includes("*") ? `<plain>${text}</plain>` : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only the text, ignoring all formatting inside
|
||||
* @param node
|
||||
|
@ -62,7 +69,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
background = "",
|
||||
): (string | string[])[] {
|
||||
if (treeAdapter.isTextNode(node)) {
|
||||
return [node.value];
|
||||
return [escapeAmbiguousMfmMarks(node.value)];
|
||||
}
|
||||
|
||||
// Skip comment or document type node
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import { type HTMLElement, Window } from "happy-dom";
|
||||
import { JSDOM } from "jsdom";
|
||||
import type * as mfm from "mfm-js";
|
||||
import katex from "katex";
|
||||
import { config } from "@/config.js";
|
||||
import { intersperse } from "@/prelude/array.js";
|
||||
import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
||||
|
||||
function toMathMl(code: string, displayMode: boolean): HTMLElement | null {
|
||||
const { window } = new Window();
|
||||
const document = window.document;
|
||||
|
||||
document.body.innerHTML = katex.renderToString(code, {
|
||||
function toMathMl(code: string, displayMode: boolean): MathMLElement | null {
|
||||
const rendered = katex.renderToString(code, {
|
||||
throwOnError: false,
|
||||
output: "mathml",
|
||||
displayMode,
|
||||
});
|
||||
|
||||
return document.querySelector("math");
|
||||
return JSDOM.fragment(rendered).querySelector("math");
|
||||
}
|
||||
|
||||
export function toHtml(
|
||||
|
@ -26,7 +22,7 @@ export function toHtml(
|
|||
return null;
|
||||
}
|
||||
|
||||
const { window } = new Window();
|
||||
const { window } = new JSDOM("");
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AlterAkaType1714099399879 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user" ADD COLUMN "alsoKnownAs" character varying(512)[]`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "user" SET "alsoKnownAs" = string_to_array("alsoKnownAsOld", ',')::character varying[]`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "user" SET "alsoKnownAs" = NULL WHERE "alsoKnownAs" = '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAsOld"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user" RENAME COLUMN "alsoKnownAs" TO "alsoKnownAsOld"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "alsoKnownAs" text`);
|
||||
await queryRunner.query(
|
||||
`UPDATE "user" SET "alsoKnownAs" = array_to_string("alsoKnownAsOld", ',')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAsOld"`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AntennaJsonbToArray1714192520471 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" RENAME COLUMN "instances" TO "instances_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" ADD COLUMN "instances" character varying(512)[] NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "instances" = ARRAY(SELECT jsonb_array_elements_text("instances_old"))::character varying(512)[]`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "instances" = '{}' WHERE "instances" = '{""}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" DROP COLUMN "instances_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" RENAME COLUMN "keywords" TO "keywords_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" ADD COLUMN "keywords" text[] NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TEMP TABLE "HMyeXPcdtQYGsSrf" ("id" character varying(32), "kws" text[])`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "HMyeXPcdtQYGsSrf" ("id", "kws") SELECT "id", array_agg("X"."w") FROM (SELECT "id", array_to_string(ARRAY(SELECT jsonb_array_elements_text("kw")), ' ') AS "w" FROM (SELECT "id", jsonb_array_elements("keywords_old") AS "kw" FROM "antenna") AS "a") AS "X" GROUP BY "id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "keywords" = "kws" FROM "HMyeXPcdtQYGsSrf" WHERE "antenna"."id" = "HMyeXPcdtQYGsSrf"."id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "keywords" = '{}' WHERE "keywords" = '{""}'`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "keywords_old"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" RENAME COLUMN "excludeKeywords" TO "excludeKeywords_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" ADD COLUMN "excludeKeywords" text[] NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TEMP TABLE "kpdsACdZTRYqLkfK" ("id" character varying(32), "kws" text[])`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "kpdsACdZTRYqLkfK" ("id", "kws") SELECT "id", array_agg("X"."w") FROM (SELECT "id", array_to_string(ARRAY(SELECT jsonb_array_elements_text("kw")), ' ') AS "w" FROM (SELECT "id", jsonb_array_elements("excludeKeywords_old") AS "kw" FROM "antenna") AS "a") AS "X" GROUP BY "id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "excludeKeywords" = "kws" FROM "kpdsACdZTRYqLkfK" WHERE "antenna"."id" = "kpdsACdZTRYqLkfK"."id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "excludeKeywords" = '{}' WHERE "excludeKeywords" = '{""}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords_old"`,
|
||||
);
|
||||
}
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "instances" = '{""}' WHERE "instances" = '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" RENAME COLUMN "instances" TO "instances_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" ADD COLUMN "instances" jsonb NOT NULL DEFAULT '[]'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "instances" = to_jsonb("instances_old")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" DROP COLUMN "instances_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "keywords" = '{""}' WHERE "keywords" = '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" RENAME COLUMN "keywords" TO "keywords_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" ADD COLUMN "keywords" jsonb NOT NULL DEFAULT '[]'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TEMP TABLE "QvPNcMitBFkqqBgm" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`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"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "keywords" = "kws" FROM "QvPNcMitBFkqqBgm" WHERE "antenna"."id" = "QvPNcMitBFkqqBgm"."id"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "keywords_old"`);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "excludeKeywords" = '{""}' WHERE "excludeKeywords" = '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" RENAME COLUMN "excludeKeywords" TO "excludeKeywords_old"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" ADD COLUMN "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TEMP TABLE "MZvVSjHzYcGXmGmz" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`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"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "antenna" SET "excludeKeywords" = "kws" FROM "MZvVSjHzYcGXmGmz" WHERE "antenna"."id" = "MZvVSjHzYcGXmGmz"."id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords_old"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DropUnusedUserprofileColumns1714259023878
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_profile" DROP COLUMN "clientData"`,
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "room"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_profile" ADD "clientData" jsonb NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.'`,
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue