Compare commits

...

43 Commits

Author SHA1 Message Date
Linca 0d27f038da Merge branch 'type-i18n' into 'develop'
dev: Add type annotations to i18n

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10773
2024-05-06 19:03:29 +00:00
naskya 14b285f882 Merge branch 'refactor/is-safe-url' into 'develop'
refactor (backend): port isValidUrl to backend-rs


See merge request firefish/firefish!10795
2024-05-06 17:11:51 +00:00
naskya baa5c402db
ci: apt-get update first & fix paths 2024-05-07 01:54:29 +09:00
naskya 5b01d3574f
refactor (backend): port isValidUrl to backend-rs 2024-05-07 00:56:37 +09:00
naskya e3a98ebc72 Merge branch 'userLang' into 'develop'
Add server-side per-user UI language

Co-authored-by: eana <coder@apps.1a23.com>

See merge request firefish/firefish!10793
2024-05-06 15:31:18 +00:00
naskya 7fe7f90350
ci: revise build config 2024-05-07 00:22:51 +09:00
naskya 8ed942e00f
chore: update auto-generated files 2024-05-06 23:13:31 +09:00
naskya ddfdd038ad
chore: update downgrade.sql 2024-05-06 23:10:39 +09:00
naskya 7fdd44cf8d
locale: update translations 2024-05-06 23:07:57 +09:00
naskya 0c4826becf
dev: copy backend-rs/index.{js,d.ts} to built/index.{js,d.ts} if not exist
https://firefish.dev/firefish/firefish/-/merge_requests/10780#note_5685
2024-05-06 22:54:10 +09:00
naskya ecd8e3d109
ci: remove git clean flags 2024-05-06 22:51:42 +09:00
naskya a3b156441a
ci: temporary fix for cargo test failure due to missing meta.json 2024-05-06 19:38:35 +09:00
naskya ecbd8a8724
ci: save node_modules and target 2024-05-06 19:23:43 +09:00
naskya 442dc33a34
ci: exec build & cargo test only for now 2024-05-06 19:08:28 +09:00
naskya c8372767fa
ci: attempt to fix permission 2024-05-06 19:00:34 +09:00
naskya 8e497b41cf
messed up 2024-05-06 18:44:28 +09:00
naskya bfdf73caeb
ci: fix permisson 2024-05-06 18:38:54 +09:00
naskya 5b18f9761c
ci: fix .git 2024-05-06 18:29:31 +09:00
naskya 641ff742bb
ci: add dependencies of sea-orm-cli 2024-05-06 18:26:50 +09:00
naskya e6121946aa
ci: another fix 2024-05-06 18:11:30 +09:00
naskya c6212ff8f4
ci: use CI_JOB_TOKEN 2024-05-06 18:06:56 +09:00
naskya d582a84c57
ci: install postgresql client 2024-05-06 17:58:26 +09:00
naskya a7978e2b08
ci: non-interactive shell option 2024-05-06 17:46:45 +09:00
naskya 766bac3dee
ci: give alias for services 2024-05-06 17:14:47 +09:00
naskya 7360736966
ci: fix typo 2024-05-06 17:07:42 +09:00
naskya e797849e9b
ci: attempt to add a CI task for merge requests 2024-05-06 17:00:36 +09:00
eana ef57735e6a fix typo 2024-05-06 05:26:38 +00:00
eana e7c33835b2 Add server-side per-user UI language 2024-05-06 05:14:44 +00:00
naskya 4e83dbd01f Merge branch 'refactor/remove-gulp' into 'develop'
refactor: replace gulp with a simple script


See merge request firefish/firefish!10791
2024-05-06 04:45:17 +00:00
naskya dd74eabae1
refactor (backend): port nodeinfo fetcher to backend-rs 2024-05-06 08:12:21 +09:00
naskya 711618b42c
test (backend-rs): add tests for nodeinfo (de)serialization 2024-05-06 05:20:13 +09:00
naskya 510207b101
refactor (backend-rs): separate nodeinfo generator and schema 2024-05-06 04:23:38 +09:00
naskya 49825853c1
refactor (backend): port nodeinfo generator to backend-rs 2024-05-06 03:01:55 +09:00
naskya 359fef0a42
chore: replace old comments 2024-05-05 21:22:57 +09:00
naskya fda81a9f91
chore: use absolute path for file operations 2024-05-05 21:04:20 +09:00
naskya c505c6df36
fix: remove old locale files 2024-05-05 20:59:26 +09:00
naskya 9a4a75bf92 Merge branch 'fix/click' into 'develop'
fix: Click event of MenuParent unexpectedly goes to underlying element

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10792
2024-05-05 11:42:25 +00:00
Linca 5e5d01d407 fix: Click event of MenuParent unexpectedly goes to underlying element
Co-authored-by: Lhcfl <Lhcfl@outlook.com>
2024-05-05 11:42:25 +00:00
naskya d114b8ec1d
chore: format 2024-05-05 14:58:56 +09:00
naskya d2471b6db7
refactor (backend-rs): replace reqwest with isahc
reqwest is feature-rich, but we will need isahc http client for push notifications (!10760)
isahc http client is also good btw :)
2024-05-05 14:53:45 +09:00
naskya 341b43ed71
refactor: replace gulp with a simple script 2024-05-05 02:19:58 +09:00
Lhcfl 4f5a6bd69b fix: https://firefish.dev/firefish/firefish/-/merge_requests/10773#note_5623 2024-04-27 21:28:35 +08:00
Lhcfl d135a362c7 dev: Add type annotations to i18n 2024-04-27 12:02:55 +08:00
65 changed files with 1617 additions and 3304 deletions

View File

@ -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.

57
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,57 @@
image: docker.io/rust:slim-bookworm
services:
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
alias: postgres
- name: docker.io/redis:7-alpine
alias: redis
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
when: always
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
cache:
paths:
- node_modules
# - /usr/local/cargo/registry/index
# - /usr/local/cargo/registry/cache
- target/debug/deps
- target/debug/build
stages:
- test
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'
default:
before_script:
- mkdir -p "${CARGO_HOME}"
- 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'
build_and_cargo_unit_test:
stage: test
script:
- pnpm install --frozen-lockfile
- pnpm run build:debug
- pnpm run migrate
- cargo test

544
Cargo.lock generated
View File

@ -124,6 +124,17 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-channel"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
dependencies = [
"concurrent-queue",
"event-listener",
"futures-core",
]
[[package]]
name = "async-stream"
version = "0.3.5"
@ -207,6 +218,7 @@ dependencies = [
"emojis",
"idna",
"image",
"isahc",
"macro_rs",
"napi",
"napi-build",
@ -218,7 +230,6 @@ dependencies = [
"rand",
"redis",
"regex",
"reqwest",
"rmp-serde",
"sea-orm",
"serde",
@ -445,6 +456,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "castaway"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
version = "1.0.95"
@ -519,6 +536,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@ -534,16 +560,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@ -661,6 +677,37 @@ dependencies = [
"sha3",
]
[[package]]
name = "curl"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"windows-sys 0.52.0",
]
[[package]]
name = "curl-sys"
version = "0.4.72+curl-8.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea"
dependencies = [
"cc",
"libc",
"libnghttp2-sys",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"windows-sys 0.52.0",
]
[[package]]
name = "der"
version = "0.7.9"
@ -793,6 +840,15 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.0.2"
@ -929,6 +985,21 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-lite"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
@ -995,25 +1066,6 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "h2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "half"
version = "2.4.1"
@ -1108,100 +1160,15 @@ dependencies = [
[[package]]
name = "http"
version = "1.1.0"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "hyper"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2",
"tokio",
"tower",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
@ -1304,6 +1271,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
@ -1316,10 +1292,31 @@ dependencies = [
]
[[package]]
name = "ipnet"
version = "2.9.0"
name = "isahc"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
dependencies = [
"async-channel",
"castaway",
"crossbeam-utils",
"curl",
"curl-sys",
"encoding_rs",
"event-listener",
"futures-lite",
"http",
"log",
"mime",
"once_cell",
"polling",
"slab",
"sluice",
"tracing",
"tracing-futures",
"url",
"waker-fn",
]
[[package]]
name = "itertools"
@ -1417,6 +1414,16 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libnghttp2-sys"
version = "0.1.10+1.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959c25552127d2e1fa72f0e52548ec04fc386e827ba71a7bd01db46a447dc135"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.27.0"
@ -1428,6 +1435,18 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "libz-sys"
version = "1.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
@ -1591,24 +1610,6 @@ dependencies = [
"libloading",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
@ -1881,6 +1882,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "parking_lot"
version = "0.12.2"
@ -2026,6 +2033,22 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "polling"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
dependencies = [
"autocfg",
"bitflags 1.3.2",
"cfg-if",
"concurrent-queue",
"libc",
"log",
"pin-project-lite",
"windows-sys 0.48.0",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -2330,49 +2353,6 @@ dependencies = [
"bytecheck",
]
[[package]]
name = "reqwest"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [
"base64 0.22.0",
"bytes",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 2.1.2",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "rgb"
version = "0.8.37"
@ -2523,22 +2503,6 @@ dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [
"base64 0.22.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@ -2680,29 +2644,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.22"
@ -2749,18 +2690,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
@ -2876,6 +2805,17 @@ dependencies = [
"autocfg",
]
[[package]]
name = "sluice"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
dependencies = [
"async-channel",
"futures-core",
"futures-io",
]
[[package]]
name = "smallvec"
version = "1.13.2"
@ -2972,7 +2912,7 @@ dependencies = [
"percent-encoding",
"rust_decimal",
"rustls",
"rustls-pemfile 1.0.4",
"rustls-pemfile",
"serde",
"serde_json",
"sha2",
@ -3229,33 +3169,6 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "sync_wrapper"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@ -3288,7 +3201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
dependencies = [
"cfg-if",
"fastrand",
"fastrand 2.0.2",
"rustix",
"windows-sys 0.52.0",
]
@ -3410,16 +3323,6 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.15"
@ -3431,20 +3334,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]]
name = "toml"
version = "0.8.12"
@ -3490,34 +3379,6 @@ dependencies = [
"winnow 0.6.7",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.40"
@ -3551,6 +3412,16 @@ dependencies = [
"valuable",
]
[[package]]
name = "tracing-futures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"pin-project",
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
@ -3576,12 +3447,6 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.17.0"
@ -3695,13 +3560,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.1"
name = "waker-fn"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690"
[[package]]
name = "wasi"
@ -3740,18 +3602,6 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
@ -3781,16 +3631,6 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "web-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.25.4"
@ -4001,16 +3841,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "wyz"
version = "0.5.1"

View File

@ -18,6 +18,7 @@ cuid2 = "0.1.2"
emojis = "0.6.2"
idna = "0.5.0"
image = "0.25.1"
isahc = "1.7.2"
nom-exif = "1.2.0"
once_cell = "1.19.0"
openssl = "0.10.64"
@ -27,7 +28,6 @@ quote = "1.0.36"
rand = "0.8.5"
redis = "0.25.3"
regex = "1.10.4"
reqwest = "0.12.4"
rmp-serde = "1.2.0"
sea-orm = "0.12.15"
serde = "1.0.198"

View File

@ -45,7 +45,7 @@ COPY packages/backend-rs/index.js packages/backend-rs/built/index.js
# Copy in the rest of the files to compile
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
RUN NODE_ENV='production' pnpm run --recursive --parallel --filter '!backend-rs' --filter '!firefish-js' 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

3
ci/cargo/config.toml Normal file
View File

@ -0,0 +1,3 @@
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]

View File

@ -2,6 +2,8 @@
Breaking changes are indicated by the :warning: icon.
- Adding `lang` to the response of `i` and the request parameter of `i/update`.
## v20240504
- :warning: Removed `release` endpoint.

View File

@ -1,6 +1,7 @@
BEGIN;
DELETE FROM "migrations" WHERE name IN (
'AddUserProfileLanguage1714888400293',
'DropUnusedIndexes1714643926317',
'AlterAkaType1714099399879',
'AddDriveFileUsage1713451569342',
@ -764,9 +765,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";

View File

@ -326,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

View File

@ -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"),
);
});

View File

@ -766,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"

View File

@ -685,6 +685,9 @@ unclip: "クリップ解除"
confirmToUnclipAlreadyClippedNote: "この投稿はすでにクリップ「{name}」に含まれています。投稿をこのクリップから除外しますか?"
public: "公開"
i18nInfo: "Firefishは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
i18nServerInfo: "新しい端末では{language}が既定の言語になります。"
i18nServerChange: "{language}に変更する。"
i18nServerSet: "新しい端末での表示言語を{language}にします。"
manageAccessTokens: "アクセストークンの管理"
accountInfo: "アカウント情報"
notesCount: "投稿の数"

View File

@ -667,6 +667,9 @@ unclip: "移除便签"
confirmToUnclipAlreadyClippedNote: "本帖已包含在便签 \"{name}\" 里。您想要将本帖从该便签中移除吗?"
public: "公开"
i18nInfo: "Firefish 已经被志愿者们翻译成了各种语言。如果您也有兴趣,可以通过 {link} 帮助翻译。"
i18nServerInfo: "新客户端将默认使用 {language}。"
i18nServerChange: "改为 {language}。"
i18nServerSet: "设定新客户端使用 {language}。"
manageAccessTokens: "管理访问令牌"
accountInfo: "账号信息"
notesCount: "帖子数量"

View File

@ -661,6 +661,9 @@ unclip: "解除摘錄"
confirmToUnclipAlreadyClippedNote: "此貼文已包含在摘錄「{name}」中。 你想將貼文從這個摘錄中排除嗎?"
public: "公開"
i18nInfo: "Firefish已經被志願者們翻譯成各種語言版本如果想要幫忙的話可以進入{link}幫助翻譯。"
i18nServerInfo: "新客戶端將默認使用 {language}。"
i18nServerChange: "改為 {language}。"
i18nServerSet: "設定新客戶端使用 {language}。"
manageAccessTokens: "管理存取權杖"
accountInfo: "帳戶資訊"
notesCount: "貼文數量"

View File

@ -9,14 +9,15 @@
"private": true,
"scripts": {
"rebuild": "pnpm run clean && pnpm run build",
"build": "pnpm node ./scripts/build.mjs && pnpm run gulp",
"build": "pnpm node ./scripts/build.mjs && pnpm run build:assets",
"build:assets": "pnpm node ./scripts/copy-assets.mjs",
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.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,10 +38,6 @@
"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": {

View File

@ -8,4 +8,5 @@ This directory contains all of the packages Firefish uses.
- `client`: Web interface written in Vue3 and TypeScript
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
- `firefish-js`: TypeScript SDK for both backend and client
- `i18n`: Typed i18n for client
- `megalodon`: TypeScript library used for partial Mastodon API compatibility

View File

@ -26,13 +26,13 @@ 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 }
regex = { workspace = true }
reqwest = { workspace = true, features = ["blocking"] }
rmp-serde = { workspace = true }
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
serde = { workspace = true, features = ["derive"] }

View File

@ -214,19 +214,26 @@ export function stringToAcct(acct: string): Acct
export function acctToString(acct: Acct): string
export function addNoteToAntenna(antennaId: string, note: Note): 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 {
@ -261,6 +268,7 @@ export interface NoteLikeForGetNoteSummary {
hasPoll: boolean
}
export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
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
@ -1122,6 +1130,7 @@ export interface UserProfile {
preventAiLearning: boolean
isIndexable: boolean
mutedPatterns: Array<string>
lang: string | null
}
export interface UserPublickey {
userId: string
@ -1148,6 +1157,106 @@ export interface Webhook {
latestStatus: number | null
}
export function initializeRustLogger(): 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 function publishToChannelStream(channelId: string, userId: string): void

View File

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -339,6 +339,7 @@ module.exports.safeForSql = safeForSql
module.exports.formatMilliseconds = formatMilliseconds
module.exports.getImageSizeFromUrl = getImageSizeFromUrl
module.exports.getNoteSummary = getNoteSummary
module.exports.isSafeUrl = isSafeUrl
module.exports.latestVersion = latestVersion
module.exports.toMastodonId = toMastodonId
module.exports.fromMastodonId = fromMastodonId
@ -364,6 +365,12 @@ module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
module.exports.initializeRustLogger = initializeRustLogger
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.publishToChannelStream = publishToChannelStream

View File

@ -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?;

View File

@ -1,6 +1,7 @@
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;
@ -9,8 +10,12 @@ use tokio::sync::Mutex;
pub enum Error {
#[error("Redis cache error: {0}")]
CacheErr(#[from] cache::Error),
#[error("Reqwest error: {0}")]
ReqwestErr(#[from] reqwest::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}")]
@ -64,7 +69,16 @@ pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
tracing::info!("retrieving image size from {}", url);
let image_bytes = http_client()?.get(url).send().await?.bytes().await?;
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();

View 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(""));
}
}

View File

@ -1,13 +1,20 @@
use crate::database::cache;
use crate::util::http_client::http_client;
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("Reqwest error: {0}")]
ReqwestErr(#[from] reqwest::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),
}
@ -21,13 +28,17 @@ async fn get_latest_version() -> Result<String, Error> {
version: String,
}
let res = http_client()?
.get(UPSTREAM_PACKAGE_JSON_URL)
.send()
.await?
.text()
.await?;
let res_parsed: Response = serde_json::from_str(&res)?;
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)
}

View File

@ -8,6 +8,7 @@ pub mod escape_sql;
pub mod format_milliseconds;
pub mod get_image_size;
pub mod get_note_summary;
pub mod is_safe_url;
pub mod latest_version;
pub mod mastodon_id;
pub mod meta;

View File

@ -78,6 +78,7 @@ pub struct Model {
pub is_indexable: bool,
#[sea_orm(column_name = "mutedPatterns")]
pub muted_patterns: Vec<String>,
pub lang: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -1,3 +1,4 @@
pub mod log;
pub mod nodeinfo;
pub mod note;
pub mod stream;

View 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"
);
}
}

View 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)?;
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)?;
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?)?)
}

View File

@ -0,0 +1,3 @@
pub mod fetch;
pub mod generate;
pub mod schema;

View 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");
}
}

View File

@ -1,24 +1,34 @@
use crate::config::CONFIG;
use isahc::{config::*, HttpClient};
use once_cell::sync::OnceCell;
use reqwest::{Client, Error, NoProxy, Proxy};
use std::time::Duration;
static CLIENT: OnceCell<Client> = OnceCell::new();
#[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),
}
pub fn http_client() -> Result<Client, Error> {
static CLIENT: OnceCell<HttpClient> = OnceCell::new();
pub fn client() -> Result<HttpClient, Error> {
CLIENT
.get_or_try_init(|| {
let mut builder = Client::builder().timeout(Duration::from_secs(5));
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 {
let mut proxy = Proxy::all(proxy_url)?;
builder = builder.proxy(Some(proxy_url.parse()?));
if let Some(proxy_bypass_hosts) = &CONFIG.proxy_bypass_hosts {
proxy = proxy.no_proxy(NoProxy::from_string(&proxy_bypass_hosts.join(",")));
builder = builder.proxy_blacklist(proxy_bypass_hosts);
}
builder = builder.proxy(proxy);
}
builder.build()
Ok(builder.build()?)
})
.cloned()
}

View File

@ -1,5 +1,3 @@
pub use http_client::http_client;
pub mod http_client;
pub mod id;
pub mod random;

View File

@ -0,0 +1,13 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserProfileLanguage1714888400293 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_profile" ADD COLUMN "lang" character varying(32)`,
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "lang"`);
}
}

View File

@ -7,10 +7,10 @@ import chalk from "chalk";
import Logger from "@/services/logger.js";
import IPCIDR from "ip-cidr";
import PrivateIp from "private-ip";
import { isValidUrl } from "./is-valid-url.js";
import { isSafeUrl } from "backend-rs";
export async function downloadUrl(url: string, path: string): Promise<void> {
if (!isValidUrl(url)) {
if (!isSafeUrl(url)) {
throw new StatusError("Invalid URL", 400);
}
@ -43,8 +43,8 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
limit: 0,
},
})
.on("redirect", (res: Got.Response, opts: Got.NormalizedOptions) => {
if (!isValidUrl(opts.url)) {
.on("redirect", (_res: Got.Response, opts: Got.NormalizedOptions) => {
if (!isSafeUrl(opts.url)) {
downloadLogger.warn(`Invalid URL: ${opts.url}`);
req.destroy();
}

View File

@ -5,7 +5,7 @@ import CacheableLookup from "cacheable-lookup";
import fetch, { type RequestRedirect } from "node-fetch";
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
import { config } from "@/config.js";
import { isValidUrl } from "./is-valid-url.js";
import { isSafeUrl } from "backend-rs";
export async function getJson(
url: string,
@ -60,7 +60,7 @@ export async function getResponse(args: {
size?: number;
redirect?: RequestRedirect;
}) {
if (!isValidUrl(args.url)) {
if (!isSafeUrl(args.url)) {
throw new StatusError("Invalid URL", 400);
}
@ -83,7 +83,7 @@ export async function getResponse(args: {
});
if (args.redirect === "manual" && [301, 302, 307, 308].includes(res.status)) {
if (!isValidUrl(res.url)) {
if (!isSafeUrl(res.url)) {
throw new StatusError("Invalid URL", 400);
}
return res;

View File

@ -1,20 +0,0 @@
export function isValidUrl(url: string | URL | undefined): boolean {
if (process.env.NODE_ENV !== "production") return true;
try {
if (url == null) return false;
const u = typeof url === "string" ? new URL(url) : url;
if (!u.protocol.match(/^https?:$/) || u.hostname === "unix") {
return false;
}
if (u.port !== "" && !["80", "443"].includes(u.port)) {
return false;
}
return true;
} catch {
return false;
}
}

View File

@ -50,6 +50,12 @@ export class UserProfile {
verified?: boolean;
}[];
@Column("varchar", {
length: 32,
nullable: true,
})
public lang: string | null;
@Column("varchar", {
length: 512,
nullable: true,

View File

@ -512,6 +512,7 @@ export const UserRepository = db.getRepository(User).extend({
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,
lang: profile!.lang,
fields: profile!.fields,
followersCount: followersCount ?? null,
followingCount: followingCount ?? null,

View File

@ -204,6 +204,12 @@ export const packedUserDetailedNotMeOnlySchema = {
optional: false,
example: "2018-03-12",
},
lang: {
type: "string",
nullable: true,
optional: false,
example: "ja-JP",
},
fields: {
type: "array",
nullable: false,

View File

@ -1,7 +1,7 @@
// https://gist.github.com/tkrotoff/a6baf96eb6b61b445a9142e5555511a0
import type { Primitive } from "type-fest";
type NullToUndefined<T> = T extends null
export type NullToUndefined<T> = T extends null
? undefined
: T extends Primitive | Function | Date | RegExp
? T
@ -15,7 +15,7 @@ type NullToUndefined<T> = T extends null
? { [K in keyof T]: NullToUndefined<T[K]> }
: unknown;
type UndefinedToNull<T> = T extends undefined
export type UndefinedToNull<T> = T extends undefined
? null
: T extends Primitive | Function | Date | RegExp
? T
@ -47,6 +47,16 @@ function _nullToUndefined<T>(obj: T): NullToUndefined<T> {
return obj as any;
}
/**
* Recursively converts all null values to undefined.
*
* @param obj object to convert
* @returns a copy of the object with all its null values converted to undefined
*/
export function fromRustObject<T>(obj: T) {
return _nullToUndefined(structuredClone(obj));
}
function _undefinedToNull<T>(obj: T): UndefinedToNull<T> {
if (obj === undefined) {
return null as any;
@ -71,6 +81,6 @@ function _undefinedToNull<T>(obj: T): UndefinedToNull<T> {
* @param obj object to convert
* @returns a copy of the object with all its undefined values converted to null
*/
export function undefinedToNull<T>(obj: T) {
export function toRustObject<T>(obj: T) {
return _undefinedToNull(structuredClone(obj));
}

View File

@ -5,8 +5,8 @@ import { StatusError, getResponse } from "@/misc/fetch.js";
import { createSignedPost, createSignedGet } from "./ap-request.js";
import type { Response } from "node-fetch";
import type { IObject } from "./type.js";
import { isValidUrl } from "@/misc/is-valid-url.js";
import { apLogger } from "@/remote/activitypub/logger.js";
import { isSafeUrl } from "backend-rs";
export default async (user: { id: User["id"] }, url: string, object: any) => {
const body = JSON.stringify(object);
@ -44,7 +44,7 @@ export async function apGet(
user?: ILocalUser,
redirects: boolean = true,
): Promise<{ finalUrl: string; content: IObject }> {
if (!isValidUrl(url)) {
if (!isSafeUrl(url)) {
throw new StatusError("Invalid URL", 400);
}

View File

@ -87,6 +87,7 @@ export const paramDef = {
description: { ...Users.descriptionSchema, nullable: true },
location: { ...Users.locationSchema, nullable: true },
birthday: { ...Users.birthdaySchema, nullable: true },
lang: { type: "string", nullable: true },
avatarId: { type: "string", format: "misskey:id", nullable: true },
bannerId: { type: "string", format: "misskey:id", nullable: true },
fields: {
@ -154,6 +155,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (ps.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) profileUpdates.description = ps.description;
if (typeof ps.lang === "string") profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.ffVisibility !== undefined)

View File

@ -1,9 +1,7 @@
import Router from "@koa/router";
import { config } from "@/config.js";
import { fetchMeta } from "backend-rs";
import { Users, Notes } from "@/models/index.js";
import { IsNull, MoreThan } from "typeorm";
import { Cache } from "@/misc/cache.js";
import { nodeinfo_2_0, nodeinfo_2_1 } from "backend-rs";
import { fromRustObject } from "@/prelude/undefined-to-null.js";
const router = new Router();
@ -22,101 +20,14 @@ export const links = [
},
];
const nodeinfo2 = async () => {
const now = Date.now();
const [meta, total, activeHalfyear, activeMonth, localPosts] =
await Promise.all([
fetchMeta(false),
Users.count({ where: { host: IsNull() } }),
Users.count({
where: {
host: IsNull(),
lastActiveDate: MoreThan(new Date(now - 15552000000)),
},
}),
Users.count({
where: {
host: IsNull(),
lastActiveDate: MoreThan(new Date(now - 2592000000)),
},
}),
Notes.count({ where: { userHost: IsNull() } }),
]);
const proxyAccount = meta.proxyAccountId
? await Users.pack(meta.proxyAccountId).catch(() => null)
: null;
return {
software: {
name: "firefish",
version: config.version,
repository: meta.repositoryUrl,
homepage: "https://firefish.dev/firefish/firefish",
},
protocols: ["activitypub"],
services: {
inbound: [] as string[],
outbound: ["atom1.0", "rss2.0"],
},
openRegistrations: !meta.disableRegistration,
usage: {
users: { total, activeHalfyear, activeMonth },
localPosts,
localComments: 0,
},
metadata: {
nodeName: meta.name,
nodeDescription: meta.description,
maintainer: {
name: meta.maintainerName,
email: meta.maintainerEmail,
},
langs: meta.langs,
tosUrl: meta.tosUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
disableLocalTimeline: meta.disableLocalTimeline,
disableRecommendedTimeline: meta.disableRecommendedTimeline,
disableGlobalTimeline: meta.disableGlobalTimeline,
emailRequiredForSignup: meta.emailRequiredForSignup,
postEditing: true,
postImports: meta.experimentalFeatures?.postImports || false,
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: config.maxNoteLength,
maxCaptionTextLength: config.maxCaptionLength,
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
themeColor: meta.themeColor || "#31748f",
},
};
};
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(
"nodeinfo",
60 * 10,
);
router.get(nodeinfo2_1path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2());
ctx.body = { version: "2.1", ...base };
ctx.set("Cache-Control", "public, max-age=600");
ctx.body = fromRustObject(await nodeinfo_2_1());
ctx.set("Cache-Control", "public, max-age=3600");
});
router.get(nodeinfo2_0path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2());
// @ts-ignore
base.software.repository = undefined;
// @ts-ignore
base.software.homepage = undefined;
ctx.body = { version: "2.0", ...base };
ctx.set("Cache-Control", "public, max-age=600");
ctx.body = fromRustObject(await nodeinfo_2_0());
ctx.set("Cache-Control", "public, max-age=3600");
});
export default router;

View File

@ -10,6 +10,7 @@ import {
import { Instances } from "@/models/index.js";
import { getFetchInstanceMetadataLock } from "@/misc/app-lock.js";
import Logger from "@/services/logger.js";
import { type Nodeinfo, fetchNodeinfo } from "backend-rs";
import { inspect } from "node:util";
const logger = new Logger("metadata", "cyan");
@ -36,7 +37,7 @@ export async function fetchInstanceMetadata(
try {
const [info, dom, manifest] = await Promise.all([
fetchNodeinfo(instance).catch(() => null),
fetchNodeinfo(instance.host).catch(() => null),
fetchDom(instance).catch(() => null),
fetchManifest(instance).catch(() => null),
]);
@ -57,30 +58,26 @@ export async function fetchInstanceMetadata(
if (info) {
updates.softwareName =
info.software?.name
?.toLowerCase()
info.software.name
.toLowerCase()
.substring(0, MAX_LENGTH_INSTANCE.softwareName) || null;
updates.softwareVersion =
info.software?.version?.substring(
info.software.version.substring(
0,
MAX_LENGTH_INSTANCE.softwareVersion,
) || null;
updates.openRegistrations = info.openRegistrations;
updates.maintainerName = info.metadata
? info.metadata.maintainer
? info.metadata.maintainer.name?.substring(
0,
MAX_LENGTH_INSTANCE.maintainerName,
) || null
: null
updates.maintainerName = info.metadata.maintainer
? info.metadata.maintainer.name?.substring(
0,
MAX_LENGTH_INSTANCE.maintainerName,
) || null
: null;
updates.maintainerEmail = info.metadata
? info.metadata.maintainer
? info.metadata.maintainer.email?.substring(
0,
MAX_LENGTH_INSTANCE.maintainerEmail,
) || null
: null
updates.maintainerEmail = info.metadata.maintainer
? info.metadata.maintainer.email?.substring(
0,
MAX_LENGTH_INSTANCE.maintainerEmail,
) || null
: null;
}
@ -115,75 +112,6 @@ export async function fetchInstanceMetadata(
}
}
type NodeInfo = {
openRegistrations?: boolean;
software?: {
name?: string;
version?: string;
};
metadata?: {
name?: string;
nodeName?: string;
nodeDescription?: string;
description?: string;
maintainer?: {
name?: string;
email?: string;
};
};
};
async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> {
logger.info(`Fetching nodeinfo of ${instance.host} ...`);
try {
const wellknown = (await getJson(
`https://${instance.host}/.well-known/nodeinfo`,
).catch((e) => {
if (e.statusCode === 404) {
throw new Error("No nodeinfo provided");
} else {
throw new Error(inspect(e));
}
})) as Record<string, unknown>;
if (wellknown.links == null || !Array.isArray(wellknown.links)) {
throw new Error("No wellknown links");
}
const links = wellknown.links as any[];
const lnik1_0 = links.find(
(link) => link.rel === "http://nodeinfo.diaspora.software/ns/schema/1.0",
);
const lnik2_0 = links.find(
(link) => link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0",
);
const lnik2_1 = links.find(
(link) => link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1",
);
const link = lnik2_1 || lnik2_0 || lnik1_0;
if (link == null) {
throw new Error("No nodeinfo link provided");
}
const info = await getJson(link.href).catch((e) => {
throw new Error(inspect(e));
});
logger.info(`Successfuly fetched nodeinfo of ${instance.host}`);
return info as NodeInfo;
} catch (e) {
logger.error(
`Failed to fetch nodeinfo of ${instance.host}:\n${inspect(e)}`,
);
throw e;
}
}
async function fetchDom(instance: Instance): Promise<Window["document"]> {
logger.info(`Fetching HTML of ${instance.host} ...`);
@ -272,7 +200,7 @@ async function fetchIconUrl(
}
async function getThemeColor(
info: NodeInfo | null,
info: Nodeinfo | null,
doc: Window["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | null> {
@ -290,7 +218,7 @@ async function getThemeColor(
}
async function getSiteName(
info: NodeInfo | null,
info: Nodeinfo | null,
doc: Window["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | undefined | null> {
@ -318,7 +246,7 @@ async function getSiteName(
}
async function getDescription(
info: NodeInfo | null,
info: Nodeinfo | null,
doc: Window["document"] | null,
manifest: Record<string, any> | null,
): Promise<string | null> {

View File

@ -66,7 +66,7 @@ import { Mutex } from "redis-semaphore";
import { langmap } from "@/misc/langmap.js";
import Logger from "@/services/logger.js";
import { inspect } from "node:util";
import { undefinedToNull } from "@/prelude/undefined-to-null.js";
import { toRustObject } from "@/prelude/undefined-to-null.js";
const logger = new Logger("create-note");
@ -404,7 +404,7 @@ export default async (
checkHitAntenna(antenna, note, user).then((hit) => {
if (hit) {
// TODO: do this more sanely
addNoteToAntenna(antenna.id, undefinedToNull(note) as Note);
addNoteToAntenna(antenna.id, toRustObject(note));
}
});
}

View File

@ -21,8 +21,6 @@
"@syuilo/aiscript": "0.17.0",
"@types/autosize": "^4.0.3",
"@types/glob": "8.1.0",
"@types/gulp": "4.0.17",
"@types/gulp-rename": "2.0.6",
"@types/insert-text-at-cursor": "^0.3.2",
"@types/katex": "0.16.7",
"@types/matter-js": "0.19.6",
@ -59,6 +57,7 @@
"gsap": "^3.12.5",
"idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0",
"i18n": "workspace:*",
"json5": "2.2.3",
"katex": "0.16.10",
"long": "^5.2.3",

View File

@ -132,6 +132,9 @@ export async function signIn(token: Account["token"], redirect?: string) {
if (_DEV_) console.log("logging as token ", token);
const newAccount = await fetchAccount(token);
localStorage.setItem("account", JSON.stringify(newAccount));
if (newAccount.lang) {
localStorage.setItem("lang", newAccount.lang);
}
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
await addAccount(newAccount.id, token);

View File

@ -130,7 +130,7 @@
v-else-if="item.type === 'parent'"
class="_button item parent"
:class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)"
@mouseenter.passive="showChildren(item, $event)"
@click.stop="showChildren(item, $event)"
>
<i
@ -318,6 +318,7 @@ function onItemMouseLeave(_item) {
async function showChildren(item: MenuParent, ev: MouseEvent) {
if (props.asDrawer) {
if (ev.type === "mouseenter") return;
os.popupMenu(item.children, (ev.currentTarget ?? ev.target) as HTMLElement);
close();
} else {

View File

@ -1,36 +1,6 @@
import { markRaw } from "vue";
import { locale } from "@/config";
class I18n<T extends Record<string, any>> {
public ts: T;
constructor(locale: T) {
this.ts = locale;
// #region BIND
this.t = this.t.bind(this);
// #endregion
}
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
public t(key: string, args?: Record<string, string | number>): string {
try {
let str = key
.split(".")
.reduce((o, i) => o[i], this.ts) as unknown as string;
if (args) {
for (const [k, v] of Object.entries(args)) {
str = str.replaceAll(`{${k}}`, v.toString());
}
}
return str;
} catch (err) {
console.warn(`missing localization '${key}'`);
return key;
}
}
}
import { I18n } from "i18n";
export const i18n = markRaw(new I18n(locale));

View File

@ -14,6 +14,12 @@
>
</template>
</I18n>
<I18n :src="i18n.ts.i18nServerInfo" v-if="serverLang" tag="span">
<template #language><strong>{{ langs.find(a => a[0] === serverLang)?.[1] ?? serverLang }}</strong></template>
</I18n>
<button class="_textButton" @click="updateServerLang" v-if="lang && lang !== serverLang">
{{i18n.t(serverLang ? "i18nServerChange" : "i18nServerSet", { language: langs.find(a => a[0] === lang)?.[1] ?? lang })}}
</button>
</template>
</FormSelect>
@ -404,6 +410,7 @@ import { deviceKind } from "@/scripts/device-kind";
import icon from "@/scripts/icon";
const lang = ref(localStorage.getItem("lang"));
const serverLang = ref(me?.lang);
const translateLang = ref(localStorage.getItem("translateLang"));
const fontSize = ref(localStorage.getItem("fontSize"));
const useSystemFont = ref(localStorage.getItem("useSystemFont") !== "f");
@ -559,6 +566,14 @@ const foldNotification = computed(
// });
// }
function updateServerLang() {
os.api("i/update", {
lang: lang.value,
}).then((i) => {
serverLang.value = i.lang;
});
}
watch(swipeOnDesktop, () => {
defaultStore.set("swipeOnMobile", true);
});

24
packages/i18n/.swcrc Normal file
View File

@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true,
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"experimental": {
"emitAssertForImportAttributes": true
},
"target": "es2022"
},
"minify": false,
"module": {
"type": "es6",
"strict": true,
"resolveFully": true
}
}

21
packages/i18n/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Firefish contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
packages/i18n/README.md Normal file
View File

@ -0,0 +1,3 @@
# i18n
## Typed i18n for firefish

View File

@ -0,0 +1,41 @@
{
"name": "i18n",
"version": "0.0.1",
"description": "Firefish i18n for TypeScript",
"homepage": "https://firefish.dev/firefish/firefish/-/tree/develop/packages/i18n",
"main": "./built/index.js",
"types": "./src/index.ts",
"type": "module",
"license": "MIT",
"scripts": {
"build:types": "node scripts/build.js",
"build": "pnpm run build:types && pnpm swc src --out-dir built --source-maps false --copy-files --strip-leading-paths",
"build:debug": "pnpm run build:types && pnpm swc src --out-dir built --source-maps true --copy-files --strip-leading-paths",
"watch": "pnpm swc src --out-dir built --source-maps true --copy-files --strip-leading-paths --watch",
"lint": "pnpm biome check --apply src",
"format": "pnpm biome format --write src",
"jest": "echo NOT_IMPLEMENTED",
"test": "pnpm jest && pnpm tsd"
},
"repository": {
"type": "git",
"url": "https://firefish.dev/firefish/firefish.git"
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.5.0",
"@swc/types": "^0.1.6",
"@types/js-yaml": "^4.0.9",
"@types/node": "20.12.7",
"typescript": "5.4.5"
},
"files": [
"built", "src"
],
"dependencies": {
"js-yaml": "4.1.0"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11"
}
}

View File

@ -0,0 +1,79 @@
/**
* Build I18n types from en-US.yml
*/
import yaml from "js-yaml";
import fs from "node:fs";
const locale = yaml.load(fs.readFileSync("../../locales/en-US.yml"));
function safeStr(str) {
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(str) ? str : `"${str}"`;
}
/**
*
* @param {*} obj
* @param {string} prefix
* @returns {string[]}
*/
function generateKeys(obj, prefix = "") {
const keys = [];
for (const [key, o] of Object.entries(obj)) {
const keyStr = safeStr(`${prefix}${key}`);
if (typeof o === 'string') {
const vars = o.match(/\{[A-Za-z0-9_]+\}/g);
if (vars == null || vars.length === 0) {
keys.push(`
/** ${o.replaceAll("\n", " \n * ") } */
${keyStr}: {
param: Record<string, never>;
result: ${JSON.stringify(o)};
}`);
} else {
keys.push(`
/** ${o.replaceAll("\n", " \n * ") } */
${keyStr}: {
param: {
${
[...new Set(vars)].map(v => `${v.slice(1, -1)}: any;`).join("\n\t\t")
}
};
result: ${JSON.stringify(o)};
}`);
}
} else if (typeof o === 'object') {
keys.push(generateKeys(o, `${prefix}${key}.`));
}
}
return keys;
}
function generateValues(obj) {
const keys = [];
for (const [key, o] of Object.entries(obj)) {
const keyStr = safeStr(key);
if (typeof o === 'string') {
// const vars = o.match(/\{[A-Za-z0-9_]+\}/g);
keys.push(`\n/** ${o.replaceAll("\n", " \n * ") } */\n${keyStr}: ${JSON.stringify(o)}`);
} else if (typeof o === 'object') {
keys.push(`\n${keyStr}: {\n\t${
generateValues(o).replaceAll("\n", "\n\t")
}\n}`);
}
}
return keys.join("\n");
}
const typestr = `
export interface I18nKeys {
${generateKeys(locale).flat(100).join("\n").replaceAll("\n", "\n\t")}
}
export interface I18nValues {
${generateValues(locale).replaceAll("\n", "\n\t")}
}`;
fs.mkdirSync("built", { recursive: true });
fs.writeFileSync("built/types.d.ts", typestr);

View File

@ -0,0 +1,63 @@
import type { I18nKeys, I18nValues } from "../built/types.d.ts";
const undefinedProxy: object = new Proxy(
{},
{
get() {
return undefinedProxy;
},
},
);
// biome-ignore lint/suspicious/noExplicitAny: used intentially
type ExplicitAny = any;
const makeProxy: <T extends object>(_any: T) => T = (target: ExplicitAny) =>
new Proxy(target, {
get(target, prop) {
if (target[prop] == null) {
return undefinedProxy;
}
if (typeof target[prop] === "object") {
return makeProxy(target[prop]);
}
return target[prop];
},
});
export class I18n {
public ts: I18nValues;
private _ts: I18nValues;
constructor(locale: I18nValues) {
this.ts = makeProxy(locale);
this._ts = locale;
// #region BIND
this.t = this.t.bind(this);
// #endregion
}
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
public t<K extends keyof I18nKeys>(
key: K,
args?: I18nKeys[K]["param"],
): I18nKeys[K]["result"] {
try {
let str = key
.split(".")
.reduce((o, i) => o[i as never], this._ts) as unknown as string;
if (args) {
for (const [k, v] of Object.entries(args)) {
str = str.replaceAll(`{${k}}`, v.toString());
}
}
return str as never;
} catch (err) {
console.warn(`missing localization '${key}'`);
return key as never;
}
}
}

View File

@ -0,0 +1,20 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "es2021",
"module": "es2022",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./built/",
"removeComments": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "test/**/*"]
}

File diff suppressed because it is too large Load Diff

View File

@ -5,3 +5,4 @@ packages:
- 'packages/sw'
- 'packages/firefish-js'
- 'packages/megalodon'
- 'packages/i18n'

View File

@ -12,6 +12,7 @@ import { execa } from "execa";
"--parallel",
"--filter=backend-rs",
"--filter=firefish-js",
"--filter=i18n",
"run",
"build",
], {
@ -26,6 +27,7 @@ import { execa } from "execa";
"--parallel",
"--filter=!backend-rs",
"--filter=!firefish-js",
"--filter=!i18n",
"run",
"build",
], {

View File

@ -25,6 +25,10 @@ import { fileURLToPath } from "node:url";
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/i18n/built"), {
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/megalodon/lib"), {
recursive: true,
force: true,

View File

@ -26,6 +26,14 @@ import { execa } from "execa";
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/i18n/node_modules"), {
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/firefish-js/node_modules"), {
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/megalodon/node_modules"), {
recursive: true,
force: true,

41
scripts/copy-assets.mjs Normal file
View File

@ -0,0 +1,41 @@
import fs from "node:fs/promises";
import path, { join } from "node:path";
import { fileURLToPath } from "node:url";
const repositoryRootDir = join(path.dirname(fileURLToPath(import.meta.url)), "../");
const file = (relativePath) => join(repositoryRootDir, relativePath);
await (async () => {
await fs.rm(file("built/_client_dist_/locales"), { recursive: true, force: true });
await Promise.all([
fs.cp(file("packages/backend/src/server/web"), file("packages/backend/built/server/web"), { recursive: true }),
fs.cp(file("custom/assets"), file("packages/backend/assets"), { recursive: true }),
fs.cp(file("packages/client/node_modules/three/examples/fonts"), file("built/_client_dist_/fonts"), { recursive: true }),
fs.mkdir(file("built/_client_dist_/locales"), { recursive: true }),
]);
const locales = (await import("../locales/index.mjs")).default;
const meta = (await import("../built/meta.json", { assert: { type: "json" } })).default;
for await (const [lang, locale] of Object.entries(locales)) {
await fs.writeFile(
file(`built/_client_dist_/locales/${lang}.${meta.version}.json`),
JSON.stringify({ ...locale, _version_: meta.version }),
"utf-8",
);
}
const js_assets = [
file("packages/backend/built/server/web/boot.js"),
file("packages/backend/built/server/web/bios.js"),
file("packages/backend/built/server/web/cli.js"),
];
for await (const js_file of js_assets) {
const content = (await fs.readFile(js_file, "utf-8"))
.replace("SUPPORTED_LANGS", JSON.stringify(Object.keys(locales)));
await fs.writeFile(js_file, content, "utf-8");
}
// TODO?: minify packages/backend/built/server/web/*.css
})();

View File

@ -1,6 +1,7 @@
import path, { join } from "node:path";
import { fileURLToPath } from "node:url";
import { execa } from "execa";
import fs from "node:fs";
(async () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -11,6 +12,7 @@ import { execa } from "execa";
"--parallel",
"--filter=backend-rs",
"--filter=firefish-js",
"--filter=i18n",
"run",
"build:debug",
], {
@ -25,6 +27,7 @@ import { execa } from "execa";
"--parallel",
"--filter=!backend-rs",
"--filter=!firefish-js",
"--filter=!i18n",
"run",
"build:debug",
], {
@ -32,4 +35,18 @@ import { execa } from "execa";
stdio: "inherit",
}
);
if (!fs.existsSync(join(__dirname, "/../packages/backend-rs/built/index.js"))) {
fs.copyFileSync(
join(__dirname, "/../packages/backend-rs/index.js"),
join(__dirname, "/../packages/backend-rs/built/index.js"),
);
console.warn("backend-rs/built/index.js has not been updated (https://github.com/napi-rs/napi-rs/issues/1768)");
}
if (!fs.existsSync(join(__dirname, "/../packages/backend-rs/built/index.d.ts"))) {
fs.copyFileSync(
join(__dirname, "/../packages/backend-rs/index.d.ts"),
join(__dirname, "/../packages/backend-rs/built/index.d.ts"),
);
}
})();

View File

@ -11,12 +11,6 @@ import { execa } from "execa";
stderr: process.stderr,
});
execa("pnpm", ["dlx", "gulp", "watch"], {
cwd: join(__dirname, "/../"),
stdout: process.stdout,
stderr: process.stderr,
});
execa("pnpm", ["--filter", "backend", "watch"], {
cwd: join(__dirname, "/../"),
stdout: process.stdout,