Compare commits

...

41 Commits

Author SHA1 Message Date
laozhoubuluo 285b681adb Merge branch 'feat/post_import_export' into 'develop'
feat: import firefish renote and reply from export, import self-reply from mastodon export

Closes #9947, #10661, and #10807

See merge request firefish/firefish!10689
2024-05-19 12:41:07 +00:00
naskya 4005307cbe Merge branch 'renovate/lock-file-maintenance' into 'develop'
chore(deps): lock file maintenance

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10874
2024-05-19 12:25:27 +00:00
naskya 2535fed09a Merge branch 'renovate/fluent-ffmpeg-2.x' into 'develop'
fix(deps): update dependency fluent-ffmpeg to v2.1.3

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10873
2024-05-19 12:10:36 +00:00
CI 7e3b46f504 chore(deps): lock file maintenance 2024-05-19 12:06:46 +00:00
CI 32c00d3014 fix(deps): update dependency fluent-ffmpeg to v2.1.3 2024-05-19 12:05:27 +00:00
naskya aa78efd20e
docs: update changelog 2024-05-19 19:42:30 +09:00
naskya dbcecf6966
docs (minor): update notice-for-admins.md 2024-05-19 19:41:55 +09:00
naskya 3f22cdbf09 Merge branch 'fix/use-timestamptz' into 'develop'
fix: store timezone agnostic datetime in database


See merge request firefish/firefish!10872
2024-05-19 10:37:24 +00:00
naskya 134bbb48ab
docs: update notice-for-admins.md 2024-05-19 19:23:28 +09:00
naskya 8174d646f9
fix (backend): add back timezone 2024-05-19 19:15:58 +09:00
naskya 84596c8313
chore (deps): use unreleased napi crate for now 2024-05-19 19:12:20 +09:00
naskya 9dbf6d07c4
fix (backend): pug config loading 2024-05-19 19:11:13 +09:00
naskya 2f4c30176d
chore (deps): correct git url 2024-05-19 13:01:24 +09:00
naskya c73ce2ff39
chore (backend-rs): don't iterate the same process 2024-05-19 12:49:45 +09:00
naskya 797c768f24
feat (backend-rs): add Mastodon push notification type 2024-05-19 12:19:58 +09:00
naskya 945465cc5e Merge branch 'renovate/napi-derive-2.x' into 'develop'
chore(deps): update rust crate napi-derive to 2.16.5

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10870
2024-05-18 21:32:23 +00:00
naskya fb232b4797 Merge branch 'renovate/got-14.x' into 'develop'
fix(deps): update dependency got to v14.3.0

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10871
2024-05-18 21:31:48 +00:00
naskya 3661c4da74 Merge branch 'fold-poll' into 'develop'
feat: fold poll notifications

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

See merge request firefish/firefish!10869
2024-05-18 21:31:21 +00:00
CI 654e71e43a fix(deps): update dependency got to v14.3.0 2024-05-18 20:06:04 +00:00
CI 819cb9a824 chore(deps): update rust crate napi-derive to 2.16.5 2024-05-18 20:05:27 +00:00
Lhcfl c18987e3b4 feat: fold poll 2024-05-18 21:09:11 +08:00
naskya 2613dfd952
ci: try overlay2 2024-05-18 21:14:47 +09:00
naskya dfc8763f0c Merge branch 'renovate/bull-4.x' into 'develop'
fix(deps): update dependency bull to v4.12.6

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10868
2024-05-18 12:07:47 +00:00
CI 3d5003acc2 fix(deps): update dependency bull to v4.12.6 2024-05-18 12:04:48 +00:00
naskya b8e5b8616a
ci: fix conditions 2024-05-18 20:49:13 +09:00
naskya 3a7c5fcdbb
ci: use buildah image directly 2024-05-18 20:43:58 +09:00
naskya 1c0d2e43b4
chore (backend-rs, minor): use crate::export 2024-05-18 19:45:38 +09:00
naskya d9ff9101a1
ci: fix typo 2024-05-18 19:39:17 +09:00
naskya 58268a2c6e
ci: cargo test no longer depends on other workspaces 2024-05-18 19:39:02 +09:00
sup39 c4a7cd6029
refactor (backend): port greet to backend-rs
Co-authored-by: naskya <m@naskya.net>
2024-05-18 17:39:45 +08:00
sup39 2a8193fa6c
meta: macro_rs -> macro-rs 2024-05-18 17:39:45 +08:00
sup39 ada5ff7e75
refactor (backend-rs): read version from package.json at compile time 2024-05-18 17:39:17 +08:00
naskya 7dd3b8ec5a
ci: buildah package on bookworm is too old 2024-05-18 16:52:30 +09:00
naskya 88890153a1 Merge branch 'renovate/eslint-monorepo' into 'develop'
chore(deps): update dependency eslint to v9.3.0

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10867
2024-05-18 05:17:08 +00:00
naskya 23f47aac38
ci: buildah prune before build 2024-05-18 14:16:31 +09:00
naskya abc9d58f7c
refactor (backend-rs): use bb8 connection pool for Redis 2024-05-18 13:20:47 +09:00
Daniel Smith ab3ca2a20b
feat (backend-rs): impl bb8::ManageConnection for RedisConnectionManager
62597aa45a/redis/src/lib.rs

Co-authored-by: Kyle Huey <khuey@kylehuey.com>
2024-05-18 13:20:47 +09:00
naskya fdc77b74ae
chore: lint 2024-05-18 13:20:46 +09:00
naskya 61562a0943
refactor (backend-rs): use async redis commands 2024-05-18 13:20:43 +09:00
CI a298302f76 chore(deps): update dependency eslint to v9.3.0 2024-05-18 04:05:11 +00:00
老周部落 469ca68e2e
feat: import firefish renote and reply from export, import self-reply from mastodon export 2024-05-12 09:42:19 +08:00
182 changed files with 1730 additions and 1055 deletions

View File

@ -59,7 +59,7 @@ test:build:
rules:
- if: $TEST == 'false'
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/backend-rs/**/*
@ -84,7 +84,7 @@ test:build:backend_ts_only:
rules:
- if: $TEST == 'false'
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/backend-rs/**/*
@ -94,7 +94,7 @@ test:build:backend_ts_only:
- Cargo.toml
- Cargo.lock
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/backend/**/*
@ -124,7 +124,7 @@ test:build:client_only:
rules:
- if: $TEST == 'false'
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/backend-rs/**/*
@ -134,7 +134,7 @@ test:build:client_only:
- Cargo.toml
- Cargo.lock
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/client/**/*
@ -157,7 +157,7 @@ test:build:client_only:
build:container:
stage: build
image: docker.io/debian:bookworm-slim
image: quay.io/buildah/stable:latest
services: []
rules:
- if: $BUILD == 'false'
@ -181,20 +181,17 @@ build:container:
optional: true
- job: test:build:client_only
optional: true
variables:
STORAGE_DRIVER: overlay2
before_script:
- apt-get update && apt-get -y upgrade
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
- buildah version
- buildah prune --all --force || true
- echo "${CI_REGISTRY_PASSWORD}" | buildah login --username "${CI_REGISTRY_USER}" --password-stdin "${CI_REGISTRY}"
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
script:
- |-
buildah build \
--isolation chroot \
--device /dev/fuse:rw \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
--cap-add all \
--platform linux/amd64 \
--layers \
--cache-to "${IMAGE_CACHE}" \
@ -209,23 +206,17 @@ cargo:test:
rules:
- if: $TEST == 'false'
when: never
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/backend-rs/**/*
- packages/macro-rs/**/*
- Cargo.toml
- Cargo.lock
- package.json
when: always
script:
- curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/cargo/bin
- pnpm install --frozen-lockfile
- mkdir -p packages/backend-rs/built
- cp packages/backend-rs/index.js packages/backend-rs/built/index.js
- cp packages/backend-rs/index.d.ts packages/backend-rs/built/index.d.ts
- pnpm --filter='!backend-rs' run build:debug
- cargo test --doc
- cargo nextest run
@ -234,9 +225,7 @@ cargo:clippy:
rules:
- if: $TEST == 'false'
when: never
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
when: never
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_PIPELINE_SOURCE == 'merge_request_event'
- if: $CI_COMMIT_BRANCH == 'develop' || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'develop'
changes:
paths:
- packages/backend-rs/**/*

View File

@ -30,6 +30,10 @@ Chiptune2.js by Simon Gündling
License: MIT
https://github.com/deskjet/chiptune2.js#license
bb8-redis by Kyle Huey
License: MIT
https://github.com/djc/bb8/blob/62597aa45ac1746780b08cb6a68cf7d65452a23a/LICENSE
Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository:
pnpm licenses list

101
Cargo.lock generated
View File

@ -85,9 +85,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.84"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18b8795de6d09abb2b178fa5a9e3bb10da935750f33449a132b328b9391b2c6a"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arbitrary"
@ -211,7 +211,9 @@ name = "backend-rs"
version = "0.0.0"
dependencies = [
"argon2",
"async-trait",
"basen",
"bb8",
"bcrypt",
"chrono",
"cuid2",
@ -219,7 +221,7 @@ dependencies = [
"idna",
"image",
"isahc",
"macro_rs",
"macro-rs",
"napi",
"napi-build",
"napi-derive",
@ -239,6 +241,7 @@ dependencies = [
"sysinfo",
"thiserror",
"tokio",
"tokio-test",
"tracing",
"tracing-subscriber",
"url",
@ -297,6 +300,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dbe4bb73fd931c4d1aaf53b35d1286c8a948ad00ec92c8e3c856f15fd027f43"
[[package]]
name = "bb8"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7c2093d15d6a1d33b1f972e1c5ea3177748742b97a5f392aa83a65262c6780"
dependencies = [
"async-trait",
"futures-channel",
"futures-util",
"parking_lot",
"tokio",
]
[[package]]
name = "bcrypt"
version = "0.15.1"
@ -484,9 +500,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
[[package]]
name = "cc"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
dependencies = [
"jobserver",
"libc",
@ -564,7 +580,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
@ -1203,9 +1223,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
@ -1622,9 +1642,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.153"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libfuzzer-sys"
@ -1718,13 +1738,15 @@ dependencies = [
]
[[package]]
name = "macro_rs"
name = "macro-rs"
version = "0.0.0"
dependencies = [
"convert_case",
"napi",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn 2.0.64",
"thiserror",
]
@ -1790,14 +1812,12 @@ dependencies = [
[[package]]
name = "napi"
version = "2.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368"
version = "3.0.0-alpha.2"
source = "git+https://github.com/napi-rs/napi-rs.git?rev=ca2cd5c35a0c39ec4a94e93c6c5695b681046df2#ca2cd5c35a0c39ec4a94e93c6c5695b681046df2"
dependencies = [
"bitflags 2.5.0",
"chrono",
"ctor",
"napi-derive",
"napi-sys",
"once_cell",
"serde",
@ -1813,9 +1833,9 @@ checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
[[package]]
name = "napi-derive"
version = "2.16.4"
version = "2.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb613535cde46cff231e53cd819c1694a32d48946bc2dda6b41174ace52ac08"
checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4"
dependencies = [
"cfg-if",
"convert_case",
@ -1827,9 +1847,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "1.0.66"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da041b19246ab4240998774e987fd9a7d92cc7406b91b5eddb6691e81feac044"
checksum = "bff2c00437f3b3266391eb5e6aa25d0029187daf5caf05b8e3271468fb5ae73e"
dependencies = [
"convert_case",
"once_cell",
@ -1843,8 +1863,7 @@ dependencies = [
[[package]]
name = "napi-sys"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
source = "git+https://github.com/napi-rs/napi-rs.git?rev=ca2cd5c35a0c39ec4a94e93c6c5695b681046df2#ca2cd5c35a0c39ec4a94e93c6c5695b681046df2"
dependencies = [
"libloading",
]
@ -2618,12 +2637,16 @@ version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6472825949c09872e8f2c50bde59fcefc17748b6be5c90fd67cd8b4daca73bfd"
dependencies = [
"async-trait",
"bytes",
"combine",
"futures-util",
"itoa",
"percent-encoding",
"pin-project-lite",
"ryu",
"sha1_smol",
"socket2",
"tokio",
"tokio-util",
"url",
]
@ -3100,12 +3123,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sha1_smol"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sha2"
version = "0.10.8"
@ -3767,6 +3784,32 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-test"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7"
dependencies = [
"async-stream",
"bytes",
"futures-core",
"tokio",
"tokio-stream",
]
[[package]]
name = "tokio-util"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "0.8.13"
@ -4082,7 +4125,7 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "web-push"
version = "0.10.1"
source = "git+https://github.com/pimeys/rust-web-push?rev=40febe4085e3cef9cdfd539c315e3e945aba0656#40febe4085e3cef9cdfd539c315e3e945aba0656"
source = "git+https://github.com/pimeys/rust-web-push.git?rev=40febe4085e3cef9cdfd539c315e3e945aba0656#40febe4085e3cef9cdfd539c315e3e945aba0656"
dependencies = [
"async-trait",
"base64 0.13.1",

View File

@ -3,14 +3,16 @@ members = ["packages/backend-rs", "packages/macro-rs"]
resolver = "2"
[workspace.dependencies]
macro_rs = { path = "packages/macro-rs" }
macro-rs = { path = "packages/macro-rs" }
napi = { version = "2.16.6", default-features = false }
napi-derive = "2.16.4"
napi = { git = "https://github.com/napi-rs/napi-rs.git", rev = "ca2cd5c35a0c39ec4a94e93c6c5695b681046df2", default-features = false }
napi-derive = "2.16.5"
napi-build = "2.1.3"
argon2 = "0.5.3"
async-trait = "0.1.80"
basen = "0.1.0"
bb8 = "0.8.3"
bcrypt = "0.15.1"
chrono = "0.4.38"
convert_case = "0.6.0"
@ -26,7 +28,7 @@ pretty_assertions = "1.4.0"
proc-macro2 = "1.0.82"
quote = "1.0.36"
rand = "0.8.5"
redis = "0.25.3"
redis = { version = "0.25.3", default-features = false }
regex = "1.10.4"
rmp-serde = "1.3.0"
sea-orm = "0.12.15"
@ -38,11 +40,12 @@ syn = "2.0.64"
sysinfo = "0.30.12"
thiserror = "1.0.61"
tokio = "1.37.0"
tokio-test = "0.4.4"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"
urlencoding = "2.1.3"
web-push = { git = "https://github.com/pimeys/rust-web-push", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656" }
web-push = { git = "https://github.com/pimeys/rust-web-push.git", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656" }
[profile.release]
lto = true

View File

@ -5,6 +5,10 @@ Critical security updates are indicated by the :warning: icon.
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
## Unreleased
- Fix bugs
## [v20240516](https://firefish.dev/firefish/firefish/-/merge_requests/10854/commits)
- Improve timeline UX (you can restore the original appearance by settings)

View File

@ -1,6 +1,7 @@
BEGIN;
DELETE FROM "migrations" WHERE name IN (
'AddBackTimezone1715351290096',
'UserprofileJsonbToArray1714270605574',
'DropUnusedUserprofileColumns1714259023878',
'AntennaJsonbToArray1714192520471',
@ -105,83 +106,6 @@ CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" p
ALTER TABLE "messaging_message" DROP CONSTRAINT "FK_535def119223ac05ad3fa9ef64b";
ALTER TABLE "messaging_message" ADD CONSTRAINT "FK_535def119223ac05ad3fa9ef64b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
-- drop-time-zone
ALTER TABLE "abuse_user_report" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "access_token" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "access_token" ALTER "lastUsedAt" TYPE timestamp with time zone;
ALTER TABLE "ad" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "ad" ALTER "expiresAt" TYPE timestamp with time zone;
ALTER TABLE "announcement" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "announcement" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "announcement_read" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "antenna" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "app" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "attestation_challenge" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "auth_session" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "blocking" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "channel" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "channel" ALTER "lastNotedAt" TYPE timestamp with time zone;
ALTER TABLE "channel_following" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "channel_note_pining" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "clip" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "drive_file" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "drive_folder" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "emoji" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "following" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "follow_request" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "gallery_like" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "gallery_post" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "gallery_post" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "instance" ALTER "caughtAt" TYPE timestamp with time zone;
ALTER TABLE "instance" ALTER "infoUpdatedAt" TYPE timestamp with time zone;
ALTER TABLE "instance" ALTER "lastCommunicatedAt" TYPE timestamp with time zone;
ALTER TABLE "instance" ALTER "latestRequestReceivedAt" TYPE timestamp with time zone;
ALTER TABLE "instance" ALTER "latestRequestSentAt" TYPE timestamp with time zone;
ALTER TABLE "messaging_message" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "moderation_log" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "muting" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "muting" ALTER "expiresAt" TYPE timestamp with time zone;
ALTER TABLE "note" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "note" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "note_edit" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "note_favorite" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "note_reaction" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "note_thread_muting" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "note_watching" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "notification" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "page" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "page" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "page_like" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "password_reset_request" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "poll" ALTER "expiresAt" TYPE timestamp with time zone;
ALTER TABLE "poll_vote" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "promo_note" ALTER "expiresAt" TYPE timestamp with time zone;
ALTER TABLE "promo_read" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "registration_ticket" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "registry_item" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "registry_item" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "renote_muting" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "reply_muting" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "signin" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "sw_subscription" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "used_username" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user" ALTER "lastActiveDate" TYPE timestamp with time zone;
ALTER TABLE "user" ALTER "lastFetchedAt" TYPE timestamp with time zone;
ALTER TABLE "user" ALTER "updatedAt" TYPE timestamp with time zone;
ALTER TABLE "user_group" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_group_invitation" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_group_invite" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_group_joining" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_ip" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_list" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_list_joining" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_note_pining" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_pending" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "user_security_key" ALTER "lastUsed" TYPE timestamp with time zone;
ALTER TABLE "webhook" ALTER "createdAt" TYPE timestamp with time zone;
ALTER TABLE "webhook" ALTER "latestSentAt" TYPE timestamp with time zone;
-- expand-note-edit
ALTER TABLE "note_edit" DROP COLUMN "emojis";

View File

@ -2,6 +2,28 @@
You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
## Unreleased
### For all users
We regret to inform you that the upgrade may take a long time to fix a regression we have introduced. The time required to upgrade should be the same as [v20240413](<https://firefish.dev/firefish/firefish/-/blob/main/docs/notice-for-admins.md#v20240413>). This is not a security fix, so please upgrade your server when you have enough time. We are sorry for the inconvenience.
<details>
There are two data types in PostgreSQL to store time: `timestamptz` (`timestamp with time zone`) and `timestamp` (`timestamp without time zone`) [[ref]](<https://www.postgresql.org/docs/current/datatype-datetime.html>).
In Node.js, we manipulate the database using [TypeORM](<https://typeorm.io/>). TypeORM handles time data as a JavaScript `Date` object. Since `Date` doesn't have timezone information [[ref]](<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_components_and_time_zones>), we don't use the timezone information in the Node.js backend, and both `timestamptz` and `timestamp` behave in the same way. (Technically, the type names are a little confusing, and `timestamptz` (`timestamp with time zone`) doesn't store the timezone data. Please read PostgreSQL documentation for more information.)
In Rust, we manipulate the database using [SeaORM](<https://www.sea-ql.org/SeaORM/>), which does distinguish between `timestamptz` and `timestamp`. `timestamptz` is converted to [`DateTime<FixedOffset>`](<https://docs.rs/chrono/latest/chrono/struct.DateTime.html>) type, whereas `timestamp` is converted to [`NaiveDateTime`](<https://docs.rs/chrono/latest/chrono/struct.NaiveDateTime.html>).
We are using [napi-rs](<https://napi.rs/>) to implement some of the backend features in Rust, which did not support `DateTime<FixedOffset>`. We used to store time data as `timestamptz`, but we converted them to `timestamp` for this reason. As we don't use timezone data, we thought this was okay, and indeed it worked fine.
However, we did not consider the case of migrating a server (hardware) to another timezone. With `timestamp`, there may be inconsistencies in the time data if you migrate your server to another system with a different timezone setting (Docker/Podman users should not be affected by this, as UTC is always used in containers unless you explicitly set one).
Therefore, we have contributed to napi-rs to add support for `DateTime<FixedOffset>` (<https://github.com/napi-rs/napi-rs/pull/2074>) and decided to migrate back from `timestamp` to `timestamptz` to properly address this problem. The migration process takes time roughly proportional to the number of stored posts.
</details>
## v20240516
### For all users

View File

@ -74,6 +74,34 @@ mentions: "Mentions"
directNotes: "Direct messages"
cw: "Content warning"
importAndExport: "Import/Export Data"
importAndExportWarn: "The Import/Export Data feature is an experimental feature and
implementation may change at any time without prior notice.\n
Due to differences in the exported data of different software versions, the actual
conditions of the import program, and the server health of the exported data link,
the imported data may be incomplete or the access permissions may not be set
correctly (for example, there is no access permission mark in the
Mastodon/Akkoma/Pleroma exported data, so all posts makes public after import),
so please be sure to check the imported data carefully integrity and configure
the correct access permissions for it."
importAndExportInfo: "Since some data cannot be obtained after the original account is
frozen or the original server goes offline, it is strongly recommendedthat you import
the data before the original account is frozen (migrated, logged out) or the original
server goes offline.\n
If the original account is frozen or the original server is offline but you have the
original images, you can try uploading them to the network disk before importing the
data, which may help with data import.\n
Since some data is obtained from its server using your current account when importing
data, data that the current account does not have permission to access will be regarded
as broken. Please make adjustments including but not limited to access permissions,
Manually following accounts and other methods allow the current account to obtain
relevant data, so that the import program can normally obtain the data it needs to
obtain to help you import.\n
Since it is impossible to confirm whether the broken link content posted by someone other
than you is posted by him/her, if there is broken link content posted by others in the
discussion thread, the related content and subsequent replies will not be imported.\n
Since data import is greatly affected by network communication, it is recommended that you
pay attention to data recovery after a period of time. If the data is still not restored,
you can try importing the same backup file again and try again."
import: "Import"
export: "Export"
files: "Files"

View File

@ -61,6 +61,16 @@ mention: "提及"
mentions: "提及"
directNotes: "私信"
importAndExport: "导入 / 导出数据"
importAndExportWarn: "导入 / 导出数据功能是一项实验性功能,实现可能会随时变化而无预先通知。\n
由于不同软件不同版本的导出数据、导入程序实际情况以及导出数据链接的服务器运行状况不同,导入的数据可能会不完整或未被正确设置访问权限
(例如 Mastodon/Akkoma/Pleroma 导出数据内无访问权限标记,因此所有帖子导入后均为公开状态),因此请务必谨慎核对导入数据的完整性,
并为其配置正确的访问权限。"
importAndExportInfo: "由于原账号冻结或者原服务器下线后部分数据无法获取,因此强烈建议您在原账号冻结(迁移、注销)或原服务器下线前导入数据。\n
在原账号冻结或者原服务器下线但您拥有原始图片的情况下,可以尝试在导入数据之前将其上传到网盘上,可能对数据导入有所帮助。\n
由于导入数据时部分数据是使用您当前账号到其服务器上获取,因此当前账号无权访问的数据会视为断链。请通过包括但不限于访问权限调整、
手动关注账户等方式让当前帐号可以获取到相关数据,以便导入程序能够正常获取到需要获取的数据从而帮助您进行导入。\n
由于无法确认非您本人发表的断链内容的是否由其本人发表,因此如果讨论串内有其他人发表的断链内容,则相关内容以及后续回复不会被导入。\n
由于数据导入受网络通信影响较大,因此建议您一段时间之后再关注数据恢复情况。如果数据仍未恢复可以尝试再次导入同样的备份文件重试一次。"
import: "导入"
export: "导出"
files: "文件"

View File

@ -13,13 +13,15 @@ ci = []
crate-type = ["cdylib", "lib"]
[dependencies]
macro_rs = { workspace = true }
macro-rs = { workspace = true }
napi = { workspace = true, optional = true, default-features = false, features = ["napi9", "tokio_rt", "chrono_date", "serde-json"] }
napi-derive = { workspace = true, optional = true }
argon2 = { workspace = true, features = ["std"] }
async-trait = { workspace = true }
basen = { workspace = true }
bb8 = { workspace = true }
bcrypt = { workspace = true }
chrono = { workspace = true }
cuid2 = { workspace = true }
@ -31,7 +33,7 @@ nom-exif = { workspace = true }
once_cell = { workspace = true }
openssl = { workspace = true, features = ["vendored"] }
rand = { workspace = true }
redis = { workspace = true }
redis = { workspace = true, default-features = false, features = ["streams", "tokio-comp"] }
regex = { workspace = true }
rmp-serde = { workspace = true }
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
@ -50,6 +52,7 @@ web-push = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tokio-test = { workspace = true }
[build-dependencies]
napi-build = { workspace = true }

View File

@ -1,5 +1,9 @@
extern crate napi_build;
fn main() {
// watch the version in the project root package.json
println!("cargo:rerun-if-changed=../../package.json");
// napi
napi_build::setup();
}

View File

@ -191,18 +191,6 @@ export interface Config {
authUrl: string
driveUrl: string
userAgent: string
clientEntry: Manifest
}
export interface Manifest {
file: string
name: string
src: string
isEntry: boolean
isDynamicEntry: boolean
imports: Array<string>
dynamicImports: Array<string>
css: Array<string>
assets: Array<string>
}
export function loadConfig(): Config
export interface Acct {
@ -211,6 +199,7 @@ export interface Acct {
}
export function stringToAcct(acct: string): Acct
export function acctToString(acct: Acct): string
export function greet(): void
export function initializeRustLogger(): void
export function showServerInfo(): void
/**
@ -323,7 +312,7 @@ export function memoryUsage(): Memory
export function storageUsage(): Storage | null
export interface AbuseUserReport {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
targetUserId: string
reporterId: string
assigneeId: string | null
@ -335,12 +324,12 @@ export interface AbuseUserReport {
}
export interface AccessToken {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
token: string
hash: string
userId: string
appId: string | null
lastUsedAt: Date | null
lastUsedAt: DateTimeWithTimeZone | null
session: string | null
name: string | null
description: string | null
@ -350,8 +339,8 @@ export interface AccessToken {
}
export interface Ad {
id: string
createdAt: Date
expiresAt: Date
createdAt: DateTimeWithTimeZone
expiresAt: DateTimeWithTimeZone
place: string
priority: string
url: string
@ -361,11 +350,11 @@ export interface Ad {
}
export interface Announcement {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
text: string
title: string
imageUrl: string | null
updatedAt: Date | null
updatedAt: DateTimeWithTimeZone | null
showPopup: boolean
isGoodNews: boolean
}
@ -373,11 +362,11 @@ export interface AnnouncementRead {
id: string
userId: string
announcementId: string
createdAt: Date
createdAt: DateTimeWithTimeZone
}
export interface Antenna {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
name: string
src: AntennaSrcEnum
@ -395,7 +384,7 @@ export interface Antenna {
}
export interface App {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string | null
secret: string
name: string
@ -407,26 +396,26 @@ export interface AttestationChallenge {
id: string
userId: string
challenge: string
createdAt: Date
createdAt: DateTimeWithTimeZone
registrationChallenge: boolean
}
export interface AuthSession {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
token: string
userId: string | null
appId: string
}
export interface Blocking {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
blockeeId: string
blockerId: string
}
export interface Channel {
id: string
createdAt: Date
lastNotedAt: Date | null
createdAt: DateTimeWithTimeZone
lastNotedAt: DateTimeWithTimeZone | null
userId: string | null
name: string
description: string | null
@ -436,19 +425,19 @@ export interface Channel {
}
export interface ChannelFollowing {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
followeeId: string
followerId: string
}
export interface ChannelNotePining {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
channelId: string
noteId: string
}
export interface Clip {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
name: string
isPublic: boolean
@ -461,7 +450,7 @@ export interface ClipNote {
}
export interface DriveFile {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string | null
userHost: string | null
md5: string
@ -490,14 +479,14 @@ export interface DriveFile {
}
export interface DriveFolder {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
name: string
userId: string | null
parentId: string | null
}
export interface Emoji {
id: string
updatedAt: Date | null
updatedAt: DateTimeWithTimeZone | null
name: string
host: string | null
originalUrl: string
@ -512,7 +501,7 @@ export interface Emoji {
}
export interface FollowRequest {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
followeeId: string
followerId: string
requestId: string | null
@ -525,7 +514,7 @@ export interface FollowRequest {
}
export interface Following {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
followeeId: string
followerId: string
followerHost: string | null
@ -537,14 +526,14 @@ export interface Following {
}
export interface GalleryLike {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
postId: string
}
export interface GalleryPost {
id: string
createdAt: Date
updatedAt: Date
createdAt: DateTimeWithTimeZone
updatedAt: DateTimeWithTimeZone
title: string
description: string | null
userId: string
@ -571,16 +560,16 @@ export interface Hashtag {
}
export interface Instance {
id: string
caughtAt: Date
caughtAt: DateTimeWithTimeZone
host: string
usersCount: number
notesCount: number
followingCount: number
followersCount: number
latestRequestSentAt: Date | null
latestRequestSentAt: DateTimeWithTimeZone | null
latestStatus: number | null
latestRequestReceivedAt: Date | null
lastCommunicatedAt: Date
latestRequestReceivedAt: DateTimeWithTimeZone | null
lastCommunicatedAt: DateTimeWithTimeZone
isNotResponding: boolean
softwareName: string | null
softwareVersion: string | null
@ -589,7 +578,7 @@ export interface Instance {
description: string | null
maintainerName: string | null
maintainerEmail: string | null
infoUpdatedAt: Date | null
infoUpdatedAt: DateTimeWithTimeZone | null
isSuspended: boolean
iconUrl: string | null
themeColor: string | null
@ -597,7 +586,7 @@ export interface Instance {
}
export interface MessagingMessage {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
recipientId: string | null
text: string | null
@ -701,7 +690,7 @@ export interface Migrations {
}
export interface ModerationLog {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
type: string
info: Json
@ -714,14 +703,14 @@ export interface MutedNote {
}
export interface Muting {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
muteeId: string
muterId: string
expiresAt: Date | null
expiresAt: DateTimeWithTimeZone | null
}
export interface Note {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
replyId: string | null
renoteId: string | null
text: string | null
@ -751,7 +740,7 @@ export interface Note {
url: string | null
channelId: string | null
threadId: string | null
updatedAt: Date | null
updatedAt: DateTimeWithTimeZone | null
lang: string | null
}
export interface NoteEdit {
@ -760,12 +749,12 @@ export interface NoteEdit {
text: string | null
cw: string | null
fileIds: Array<string>
updatedAt: Date
updatedAt: DateTimeWithTimeZone
emojis: Array<string>
}
export interface NoteFavorite {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
noteId: string
}
@ -776,14 +765,14 @@ export interface NoteFile {
}
export interface NoteReaction {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
noteId: string
reaction: string
}
export interface NoteThreadMuting {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
threadId: string
}
@ -798,14 +787,14 @@ export interface NoteUnread {
}
export interface NoteWatching {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
noteId: string
noteUserId: string
}
export interface Notification {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
notifieeId: string
notifierId: string | null
isRead: boolean
@ -822,8 +811,8 @@ export interface Notification {
}
export interface Page {
id: string
createdAt: Date
updatedAt: Date
createdAt: DateTimeWithTimeZone
updatedAt: DateTimeWithTimeZone
title: string
name: string
summary: string | null
@ -842,19 +831,19 @@ export interface Page {
}
export interface PageLike {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
pageId: string
}
export interface PasswordResetRequest {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
token: string
userId: string
}
export interface Poll {
noteId: string
expiresAt: Date | null
expiresAt: DateTimeWithTimeZone | null
multiple: boolean
choices: Array<string>
votes: Array<number>
@ -864,31 +853,31 @@ export interface Poll {
}
export interface PollVote {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
noteId: string
choice: number
}
export interface PromoNote {
noteId: string
expiresAt: Date
expiresAt: DateTimeWithTimeZone
userId: string
}
export interface PromoRead {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
noteId: string
}
export interface RegistrationTicket {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
code: string
}
export interface RegistryItem {
id: string
createdAt: Date
updatedAt: Date
createdAt: DateTimeWithTimeZone
updatedAt: DateTimeWithTimeZone
userId: string
key: string
scope: Array<string>
@ -902,13 +891,13 @@ export interface Relay {
}
export interface RenoteMuting {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
muteeId: string
muterId: string
}
export interface ReplyMuting {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
muteeId: string
muterId: string
}
@ -994,7 +983,7 @@ export enum UserProfileMutingnotificationtypesEnum {
}
export interface Signin {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
ip: string
headers: Json
@ -1002,7 +991,7 @@ export interface Signin {
}
export interface SwSubscription {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
endpoint: string
auth: string
@ -1011,13 +1000,13 @@ export interface SwSubscription {
}
export interface UsedUsername {
username: string
createdAt: Date
createdAt: DateTimeWithTimeZone
}
export interface User {
id: string
createdAt: Date
updatedAt: Date | null
lastFetchedAt: Date | null
createdAt: DateTimeWithTimeZone
updatedAt: DateTimeWithTimeZone | null
lastFetchedAt: DateTimeWithTimeZone | null
username: string
usernameLower: string
name: string | null
@ -1043,7 +1032,7 @@ export interface User {
token: string | null
isExplorable: boolean
followersUri: string | null
lastActiveDate: Date | null
lastActiveDate: DateTimeWithTimeZone | null
hideOnlineStatus: boolean
isDeleted: boolean
driveCapacityOverrideMb: number | null
@ -1055,32 +1044,32 @@ export interface User {
}
export interface UserGroup {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
name: string
userId: string
isPrivate: boolean
}
export interface UserGroupInvitation {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
userGroupId: string
}
export interface UserGroupInvite {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
userGroupId: string
}
export interface UserGroupJoining {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
userGroupId: string
}
export interface UserIp {
id: number
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
ip: string
}
@ -1091,25 +1080,25 @@ export interface UserKeypair {
}
export interface UserList {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
name: string
}
export interface UserListJoining {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
userListId: string
}
export interface UserNotePining {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
noteId: string
}
export interface UserPending {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
code: string
username: string
email: string
@ -1161,19 +1150,19 @@ export interface UserSecurityKey {
id: string
userId: string
publicKey: string
lastUsed: Date
lastUsed: DateTimeWithTimeZone
name: string
}
export interface Webhook {
id: string
createdAt: Date
createdAt: DateTimeWithTimeZone
userId: string
name: string
on: Array<string>
url: string
secret: string
active: boolean
latestSentAt: Date | null
latestSentAt: DateTimeWithTimeZone | null
latestStatus: number | null
}
export function updateAntennasOnNewNote(note: Note, noteAuthor: Acct, noteMutedUsers: Array<string>): Promise<void>
@ -1285,22 +1274,23 @@ export enum PushNotificationKind {
ReadAllChats = 'readAllChats',
ReadAllChatsInTheRoom = 'readAllChatsInTheRoom',
ReadNotifications = 'readNotifications',
ReadAllNotifications = 'readAllNotifications'
ReadAllNotifications = 'readAllNotifications',
Mastodon = 'mastodon'
}
export function sendPushNotification(receiverUserId: string, kind: PushNotificationKind, content: any): Promise<void>
export function publishToChannelStream(channelId: string, userId: string): void
export function publishToChannelStream(channelId: string, userId: string): Promise<void>
export enum ChatEvent {
Message = 'message',
Read = 'read',
Deleted = 'deleted',
Typing = 'typing'
}
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): void
export function publishToChatStream(senderUserId: string, receiverUserId: string, kind: ChatEvent, object: any): Promise<void>
export enum ChatIndexEvent {
Message = 'message',
Read = 'read'
}
export function publishToChatIndexStream(userId: string, kind: ChatIndexEvent, object: any): void
export function publishToChatIndexStream(userId: string, kind: ChatIndexEvent, object: any): Promise<void>
export interface PackedEmoji {
id: string
aliases: Array<string>
@ -1312,15 +1302,15 @@ export interface PackedEmoji {
width: number | null
height: number | null
}
export function publishToBroadcastStream(emoji: PackedEmoji): void
export function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): void
export function publishToBroadcastStream(emoji: PackedEmoji): Promise<void>
export function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): Promise<void>
export interface AbuseUserReportLike {
id: string
targetUserId: string
reporterId: string
comment: string
}
export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): void
export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): Promise<void>
export function getTimestamp(id: string): number
/**
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.

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, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, updateAntennasOnNewNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, updateAntennasOnNewNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -323,6 +323,7 @@ module.exports.loadEnv = loadEnv
module.exports.loadConfig = loadConfig
module.exports.stringToAcct = stringToAcct
module.exports.acctToString = acctToString
module.exports.greet = greet
module.exports.initializeRustLogger = initializeRustLogger
module.exports.showServerInfo = showServerInfo
module.exports.isBlockedServer = isBlockedServer

View File

@ -3,6 +3,8 @@ use serde::Deserialize;
use std::env;
use std::fs;
pub const VERSION: &str = macro_rs::read_version_from_package_json!();
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object, use_nullable = false)]
@ -231,34 +233,6 @@ pub struct Config {
pub auth_url: String,
pub drive_url: String,
pub user_agent: String,
pub client_entry: Manifest,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Meta {
pub version: String,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct ManifestJson {
#[serde(rename = "src/init.ts")]
pub init_ts: Manifest,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object, use_nullable = false)]
pub struct Manifest {
pub file: String,
pub name: String,
pub src: String,
pub is_entry: bool,
pub is_dynamic_entry: bool,
pub imports: Vec<String>,
pub dynamic_imports: Vec<String>,
pub css: Vec<String>,
pub assets: Vec<String>,
}
fn read_config_file() -> ServerConfig {
@ -280,28 +254,10 @@ fn read_config_file() -> ServerConfig {
data
}
fn read_meta() -> Meta {
let cwd = env::current_dir().unwrap();
let meta_json = fs::File::open(cwd.join("../../built/meta.json"))
.expect("Failed to open 'built/meta.json'");
serde_json::from_reader(meta_json).expect("Failed to parse built/meta.json")
}
fn read_manifest() -> Manifest {
let cwd = env::current_dir().unwrap();
let manifest_json = fs::File::open(cwd.join("../../built/_client_dist_/manifest.json"))
.expect("Failed to open 'built/_client_dist_/manifest.json'");
let manifest: ManifestJson = serde_json::from_reader(manifest_json)
.expect("Failed to parse built/_client_dist_/manifest.json");
manifest.init_ts
}
#[crate::export]
pub fn load_config() -> Config {
let server_config = read_config_file();
let version = read_meta().version;
let manifest = read_manifest();
let version = VERSION.to_owned();
let url = url::Url::parse(&server_config.url).expect("Config url is invalid");
let hostname = url
.host_str()
@ -379,7 +335,6 @@ pub fn load_config() -> Config {
redis_key_prefix,
scheme,
ws_scheme,
client_entry: manifest,
}
}

View File

@ -1,5 +1,5 @@
use crate::database::{redis_conn, redis_key};
use redis::{Commands, RedisError};
use crate::database::{redis_conn, redis_key, RedisConnError};
use redis::{AsyncCommands, RedisError};
use serde::{Deserialize, Serialize};
#[derive(strum::Display, Debug)]
@ -18,11 +18,13 @@ pub enum Category {
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Redis error: {0}")]
RedisError(#[from] RedisError),
RedisErr(#[from] RedisError),
#[error("Redis connection error: {0}")]
RedisConnErr(#[from] RedisConnError),
#[error("Data serialization error: {0}")]
SerializeError(#[from] rmp_serde::encode::Error),
SerializeErr(#[from] rmp_serde::encode::Error),
#[error("Data deserialization error: {0}")]
DeserializeError(#[from] rmp_serde::decode::Error),
DeserializeErr(#[from] rmp_serde::decode::Error),
}
#[inline]
@ -54,26 +56,31 @@ fn wildcard(category: Category) -> String {
///
/// ```
/// # use backend_rs::database::cache;
/// # tokio_test::block_on(async {
/// let key = "apple";
/// let data = "I want to cache this string".to_string();
///
/// // caches the data for 10 seconds
/// cache::set(key, &data, 10);
/// cache::set(key, &data, 10).await;
///
/// // get the cache
/// let cached_data = cache::get::<String>(key).unwrap();
/// let cached_data = cache::get::<String>(key).await.unwrap();
/// assert_eq!(data, cached_data.unwrap());
/// # })
/// ```
pub fn set<V: for<'a> Deserialize<'a> + Serialize>(
pub async fn set<V: for<'a> Deserialize<'a> + Serialize>(
key: &str,
value: &V,
expire_seconds: u64,
) -> Result<(), Error> {
redis_conn()?.set_ex(
prefix_key(key),
rmp_serde::encode::to_vec(&value)?,
expire_seconds,
)?;
redis_conn()
.await?
.set_ex(
prefix_key(key),
rmp_serde::encode::to_vec(&value)?,
expire_seconds,
)
.await?;
Ok(())
}
@ -90,22 +97,24 @@ pub fn set<V: for<'a> Deserialize<'a> + Serialize>(
///
/// ```
/// # use backend_rs::database::cache;
/// # tokio_test::block_on(async {
/// let key = "banana";
/// let data = "I want to cache this string".to_string();
///
/// // set cache
/// cache::set(key, &data, 10).unwrap();
/// cache::set(key, &data, 10).await.unwrap();
///
/// // get cache
/// let cached_data = cache::get::<String>(key).unwrap();
/// let cached_data = cache::get::<String>(key).await.unwrap();
/// assert_eq!(data, cached_data.unwrap());
///
/// // get nonexistent (or expired) cache
/// let no_cache = cache::get::<String>("nonexistent").unwrap();
/// let no_cache = cache::get::<String>("nonexistent").await.unwrap();
/// assert!(no_cache.is_none());
/// # })
/// ```
pub fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
let serialized_value: Option<Vec<u8>> = redis_conn()?.get(prefix_key(key))?;
pub async fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V>, Error> {
let serialized_value: Option<Vec<u8>> = redis_conn().await?.get(prefix_key(key)).await?;
Ok(match serialized_value {
Some(v) => Some(rmp_serde::from_slice::<V>(v.as_ref())?),
None => None,
@ -125,22 +134,24 @@ pub fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Option<V
///
/// ```
/// # use backend_rs::database::cache;
/// # tokio_test::block_on(async {
/// let key = "chocolate";
/// let value = "I want to cache this string".to_string();
///
/// // set cache
/// cache::set(key, &value, 10).unwrap();
/// cache::set(key, &value, 10).await.unwrap();
///
/// // delete the cache
/// cache::delete("foo").unwrap();
/// cache::delete("nonexistent").unwrap(); // this is okay
/// cache::delete("foo").await.unwrap();
/// cache::delete("nonexistent").await.unwrap(); // this is okay
///
/// // the cache is gone
/// let cached_value = cache::get::<String>("foo").unwrap();
/// let cached_value = cache::get::<String>("foo").await.unwrap();
/// assert!(cached_value.is_none());
/// # })
/// ```
pub fn delete(key: &str) -> Result<(), Error> {
Ok(redis_conn()?.del(prefix_key(key))?)
pub async fn delete(key: &str) -> Result<(), Error> {
Ok(redis_conn().await?.del(prefix_key(key)).await?)
}
/// Sets a Redis cache under a `category`.
@ -154,13 +165,13 @@ pub fn delete(key: &str) -> Result<(), Error> {
/// * `key` - key (will be prefixed automatically)
/// * `value` - (de)serializable value
/// * `expire_seconds` - TTL
pub fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
pub async fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
category: Category,
key: &str,
value: &V,
expire_seconds: u64,
) -> Result<(), Error> {
set(&categorize(category, key), value, expire_seconds)
set(&categorize(category, key), value, expire_seconds).await
}
/// Gets a Redis cache under a `category`.
@ -171,11 +182,11 @@ pub fn set_one<V: for<'a> Deserialize<'a> + Serialize>(
///
/// * `category` - one of [Category]
/// * `key` - key (will be prefixed automatically)
pub fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
pub async fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
category: Category,
key: &str,
) -> Result<Option<V>, Error> {
get(&categorize(category, key))
get(&categorize(category, key)).await
}
/// Deletes a Redis cache under a `category`.
@ -186,8 +197,8 @@ pub fn get_one<V: for<'a> Deserialize<'a> + Serialize>(
///
/// * `category` - one of [Category]
/// * `key` - key (will be prefixed automatically)
pub fn delete_one(category: Category, key: &str) -> Result<(), Error> {
delete(&categorize(category, key))
pub async fn delete_one(category: Category, key: &str) -> Result<(), Error> {
delete(&categorize(category, key)).await
}
/// Deletes all Redis caches under a `category`.
@ -195,28 +206,27 @@ pub fn delete_one(category: Category, key: &str) -> Result<(), Error> {
/// ## Arguments
///
/// * `category` - one of [Category]
pub fn delete_all(category: Category) -> Result<(), Error> {
let mut redis = redis_conn()?;
let keys: Vec<Vec<u8>> = redis.keys(wildcard(category))?;
pub async fn delete_all(category: Category) -> Result<(), Error> {
let mut redis = redis_conn().await?;
let keys: Vec<Vec<u8>> = redis.keys(wildcard(category)).await?;
if !keys.is_empty() {
redis.del(keys)?
redis.del(keys).await?
}
Ok(())
}
// TODO: set_all(), get_all()
// TODO: get_all()
#[cfg(test)]
mod unit_test {
use crate::database::cache::delete_one;
use super::{delete_all, get, get_one, set, set_one, Category::Test};
use crate::database::cache::delete_one;
use pretty_assertions::assert_eq;
#[test]
fn set_get_expire() {
#[tokio::test]
async fn set_get_expire() {
#[derive(serde::Deserialize, serde::Serialize, PartialEq, Debug)]
struct Data {
id: u32,
@ -235,13 +245,13 @@ mod unit_test {
kind: "prime number".to_string(),
};
set(key_1, &value_1, 1).unwrap();
set(key_2, &value_2, 1).unwrap();
set(key_3, &value_3, 1).unwrap();
set(key_1, &value_1, 1).await.unwrap();
set(key_2, &value_2, 1).await.unwrap();
set(key_3, &value_3, 1).await.unwrap();
let cached_value_1: Vec<i32> = get(key_1).unwrap().unwrap();
let cached_value_2: String = get(key_2).unwrap().unwrap();
let cached_value_3: Data = get(key_3).unwrap().unwrap();
let cached_value_1: Vec<i32> = get(key_1).await.unwrap().unwrap();
let cached_value_2: String = get(key_2).await.unwrap().unwrap();
let cached_value_3: Data = get(key_3).await.unwrap().unwrap();
assert_eq!(value_1, cached_value_1);
assert_eq!(value_2, cached_value_2);
@ -250,17 +260,17 @@ mod unit_test {
// wait for the cache to expire
std::thread::sleep(std::time::Duration::from_millis(1100));
let expired_value_1: Option<Vec<i32>> = get(key_1).unwrap();
let expired_value_2: Option<Vec<i32>> = get(key_2).unwrap();
let expired_value_3: Option<Vec<i32>> = get(key_3).unwrap();
let expired_value_1: Option<Vec<i32>> = get(key_1).await.unwrap();
let expired_value_2: Option<Vec<i32>> = get(key_2).await.unwrap();
let expired_value_3: Option<Vec<i32>> = get(key_3).await.unwrap();
assert!(expired_value_1.is_none());
assert!(expired_value_2.is_none());
assert!(expired_value_3.is_none());
}
#[test]
fn use_category() {
#[tokio::test]
async fn use_category() {
let key_1 = "fire";
let key_2 = "fish";
let key_3 = "awawa";
@ -269,24 +279,30 @@ mod unit_test {
let value_2 = 998244353u32;
let value_3 = 'あ';
set_one(Test, key_1, &value_1, 5 * 60).unwrap();
set_one(Test, key_2, &value_2, 5 * 60).unwrap();
set_one(Test, key_3, &value_3, 5 * 60).unwrap();
set_one(Test, key_1, &value_1, 5 * 60).await.unwrap();
set_one(Test, key_2, &value_2, 5 * 60).await.unwrap();
set_one(Test, key_3, &value_3, 5 * 60).await.unwrap();
assert_eq!(get_one::<String>(Test, key_1).unwrap().unwrap(), value_1);
assert_eq!(get_one::<u32>(Test, key_2).unwrap().unwrap(), value_2);
assert_eq!(get_one::<char>(Test, key_3).unwrap().unwrap(), value_3);
assert_eq!(
get_one::<String>(Test, key_1).await.unwrap().unwrap(),
value_1
);
assert_eq!(get_one::<u32>(Test, key_2).await.unwrap().unwrap(), value_2);
assert_eq!(
get_one::<char>(Test, key_3).await.unwrap().unwrap(),
value_3
);
delete_one(Test, key_1).unwrap();
delete_one(Test, key_1).await.unwrap();
assert!(get_one::<String>(Test, key_1).unwrap().is_none());
assert!(get_one::<u32>(Test, key_2).unwrap().is_some());
assert!(get_one::<char>(Test, key_3).unwrap().is_some());
assert!(get_one::<String>(Test, key_1).await.unwrap().is_none());
assert!(get_one::<u32>(Test, key_2).await.unwrap().is_some());
assert!(get_one::<char>(Test, key_3).await.unwrap().is_some());
delete_all(Test).unwrap();
delete_all(Test).await.unwrap();
assert!(get_one::<String>(Test, key_1).unwrap().is_none());
assert!(get_one::<u32>(Test, key_2).unwrap().is_none());
assert!(get_one::<char>(Test, key_3).unwrap().is_none());
assert!(get_one::<String>(Test, key_1).await.unwrap().is_none());
assert!(get_one::<u32>(Test, key_2).await.unwrap().is_none());
assert!(get_one::<char>(Test, key_3).await.unwrap().is_none());
}
}

View File

@ -1,6 +1,7 @@
pub use postgresql::db_conn;
pub use redis::key as redis_key;
pub use redis::redis_conn;
pub use redis::RedisConnError;
pub mod cache;
pub mod postgresql;

View File

@ -1,10 +1,50 @@
use crate::config::CONFIG;
use once_cell::sync::OnceCell;
use redis::{Client, Connection, RedisError};
use async_trait::async_trait;
use bb8::{ManageConnection, Pool, PooledConnection, RunError};
use redis::{aio::MultiplexedConnection, Client, ErrorKind, IntoConnectionInfo, RedisError};
use tokio::sync::OnceCell;
static REDIS_CLIENT: OnceCell<Client> = OnceCell::new();
/// A `bb8::ManageConnection` for `redis::Client::get_multiplexed_async_connection`.
#[derive(Clone, Debug)]
pub struct RedisConnectionManager {
client: Client,
}
fn init_redis() -> Result<Client, RedisError> {
impl RedisConnectionManager {
/// Create a new `RedisConnectionManager`.
/// See `redis::Client::open` for a description of the parameter types.
pub fn new<T: IntoConnectionInfo>(info: T) -> Result<Self, RedisError> {
Ok(Self {
client: Client::open(info.into_connection_info()?)?,
})
}
}
#[async_trait]
impl ManageConnection for RedisConnectionManager {
type Connection = MultiplexedConnection;
type Error = RedisError;
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
self.client.get_multiplexed_async_connection().await
}
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
let pong: String = redis::cmd("PING").query_async(conn).await?;
match pong.as_str() {
"PONG" => Ok(()),
_ => Err((ErrorKind::ResponseError, "ping request").into()),
}
}
fn has_broken(&self, _: &mut Self::Connection) -> bool {
false
}
}
static CONN_POOL: OnceCell<Pool<RedisConnectionManager>> = OnceCell::const_new();
async fn init_conn_pool() -> Result<(), RedisError> {
let redis_url = {
let mut params = vec!["redis://".to_owned()];
@ -27,16 +67,40 @@ fn init_redis() -> Result<Client, RedisError> {
params.concat()
};
tracing::info!("Initializing Redis client");
tracing::info!("Initializing connection manager");
let manager = RedisConnectionManager::new(redis_url)?;
Client::open(redis_url)
tracing::info!("Creating connection pool");
let pool = Pool::builder().build(manager).await?;
CONN_POOL.get_or_init(|| async { pool }).await;
Ok(())
}
pub fn redis_conn() -> Result<Connection, RedisError> {
match REDIS_CLIENT.get() {
Some(client) => Ok(client.get_connection()?),
None => init_redis()?.get_connection(),
#[derive(thiserror::Error, Debug)]
pub enum RedisConnError {
#[error("Failed to initialize Redis connection pool: {0}")]
RedisErr(RedisError),
#[error("Redis connection pool error: {0}")]
Bb8PoolErr(RunError<RedisError>),
}
pub async fn redis_conn(
) -> Result<PooledConnection<'static, RedisConnectionManager>, RedisConnError> {
if !CONN_POOL.initialized() {
let init_res = init_conn_pool().await;
if let Err(err) = init_res {
return Err(RedisConnError::RedisErr(err));
}
}
CONN_POOL
.get()
.unwrap()
.get()
.await
.map_err(RedisConnError::Bb8PoolErr)
}
/// prefix redis key
@ -49,23 +113,26 @@ pub fn key(key: impl ToString) -> String {
mod unit_test {
use super::redis_conn;
use pretty_assertions::assert_eq;
use redis::Commands;
use redis::AsyncCommands;
#[test]
fn connect() {
assert!(redis_conn().is_ok());
assert!(redis_conn().is_ok());
#[tokio::test]
async fn connect() {
assert!(redis_conn().await.is_ok());
assert!(redis_conn().await.is_ok());
}
#[test]
fn access() {
let mut redis = redis_conn().unwrap();
#[tokio::test]
async fn access() {
let mut redis = redis_conn().await.unwrap();
let key = "CARGO_UNIT_TEST_KEY";
let value = "CARGO_UNIT_TEST_VALUE";
assert_eq!(redis.set::<&str, &str, String>(key, value).unwrap(), "OK");
assert_eq!(redis.get::<&str, String>(key).unwrap(), value);
assert_eq!(redis.del::<&str, u32>(key).unwrap(), 1);
assert_eq!(
redis.set::<&str, &str, String>(key, value).await.unwrap(),
"OK"
);
assert_eq!(redis.get::<&str, String>(key).await.unwrap(), value);
assert_eq!(redis.del::<&str, u32>(key).await.unwrap(), 1);
}
}

View File

@ -0,0 +1,20 @@
use crate::config::server::VERSION;
const GREETING_MESSAGE: &str = "\
Firefish is an open-source decentralized microblogging platform.
If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish
";
#[crate::export]
pub fn greet() {
println!("{}", GREETING_MESSAGE);
tracing::info!("Welcome to Firefish!");
tracing::info!("Firefish {VERSION}");
}

View File

@ -1,2 +1,3 @@
pub mod greet;
pub mod log;
pub mod system_info;

View File

@ -104,7 +104,8 @@ pub async fn check_hit_antenna(
let db = db_conn().await?;
let blocked_user_ids: Vec<String> = cache::get_one(cache::Category::Block, &note.user_id)?
let blocked_user_ids: Vec<String> = cache::get_one(cache::Category::Block, &note.user_id)
.await?
.unwrap_or({
// cache miss
let blocks = blocking::Entity::find()
@ -114,7 +115,7 @@ pub async fn check_hit_antenna(
.into_tuple::<String>()
.all(db)
.await?;
cache::set_one(cache::Category::Block, &note.user_id, &blocks, 10 * 60)?;
cache::set_one(cache::Category::Block, &note.user_id, &blocks, 10 * 60).await?;
blocks
});
@ -125,23 +126,26 @@ pub async fn check_hit_antenna(
if [NoteVisibilityEnum::Home, NoteVisibilityEnum::Followers].contains(&note.visibility) {
let following_user_ids: Vec<String> =
cache::get_one(cache::Category::Follow, &antenna.user_id)?.unwrap_or({
// cache miss
let following = following::Entity::find()
.select_only()
.column(following::Column::FolloweeId)
.filter(following::Column::FollowerId.eq(&antenna.user_id))
.into_tuple::<String>()
.all(db)
cache::get_one(cache::Category::Follow, &antenna.user_id)
.await?
.unwrap_or({
// cache miss
let following = following::Entity::find()
.select_only()
.column(following::Column::FolloweeId)
.filter(following::Column::FollowerId.eq(&antenna.user_id))
.into_tuple::<String>()
.all(db)
.await?;
cache::set_one(
cache::Category::Follow,
&antenna.user_id,
&following,
10 * 60,
)
.await?;
cache::set_one(
cache::Category::Follow,
&antenna.user_id,
&following,
10 * 60,
)?;
following
});
following
});
// if the antenna owner is not following the note author, return false
if !following_user_ids.contains(&note.user_id) {

View File

@ -61,122 +61,106 @@ mod unit_test {
let katakana_2 = r#"/[ア-ン]/u"#.to_string();
let emoji = r#"/[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/u"#.to_string();
assert!(check_word_mute_impl(
&texts,
&vec![],
&vec!["/the/i".to_string()]
));
assert!(check_word_mute_impl(&texts, &[], &["/the/i".to_string()]));
assert!(!check_word_mute_impl(
&texts,
&vec![],
&vec!["/the/".to_string()]
));
assert!(!check_word_mute_impl(&texts, &[], &["/the/".to_string()]));
assert!(check_word_mute_impl(&texts, &[], &["/QuICk/i".to_string()]));
assert!(!check_word_mute_impl(&texts, &[], &["/QuICk/".to_string()]));
assert!(check_word_mute_impl(
&texts,
&vec![],
&vec!["/QuICk/i".to_string()]
));
assert!(!check_word_mute_impl(
&texts,
&vec![],
&vec!["/QuICk/".to_string()]
));
assert!(check_word_mute_impl(
&texts,
&vec![
&[
"".to_string(),
"有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()
],
&vec![]
&[]
));
assert!(!check_word_mute_impl(
&texts,
&vec!["有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()],
&vec![]
&["有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()],
&[]
));
assert!(!check_word_mute_impl(
&texts,
&vec![
&[
"有為の奥山".to_string(),
"今日越えて".to_string(),
"浅き夢見し".to_string(),
"酔ひもせず".to_string()
],
&vec![]
&[]
));
assert!(check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "mastodon".to_string()],
&vec![hiragana_1.clone()]
&["yellow fox".to_string(), "mastodon".to_string()],
&[hiragana_1.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "mastodon".to_string()],
&vec![hiragana_2.clone()]
&["yellow fox".to_string(), "mastodon".to_string()],
&[hiragana_2.clone()]
));
assert!(!check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "mastodon".to_string()],
&vec![katakana_1.clone()]
&["yellow fox".to_string(), "mastodon".to_string()],
&[katakana_1.clone()]
));
assert!(!check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "mastodon".to_string()],
&vec![katakana_2.clone()]
&["yellow fox".to_string(), "mastodon".to_string()],
&[katakana_2.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["brown fox".to_string(), "mastodon".to_string()],
&vec![katakana_1.clone()]
&["brown fox".to_string(), "mastodon".to_string()],
&[katakana_1.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["brown fox".to_string(), "mastodon".to_string()],
&vec![katakana_2.clone()]
&["brown fox".to_string(), "mastodon".to_string()],
&[katakana_2.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "dog".to_string()],
&vec![katakana_1.clone()]
&["yellow fox".to_string(), "dog".to_string()],
&[katakana_1.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "dog".to_string()],
&vec![katakana_2.clone()]
&["yellow fox".to_string(), "dog".to_string()],
&[katakana_2.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["yellow fox".to_string(), "mastodon".to_string()],
&vec![hiragana_1.clone(), katakana_1.clone()]
&["yellow fox".to_string(), "mastodon".to_string()],
&[hiragana_1.clone(), katakana_1.clone()]
));
assert!(check_word_mute_impl(
&texts,
&vec!["😇".to_string(), "🥲".to_string(), "🥴".to_string()],
&vec![]
&["😇".to_string(), "🥲".to_string(), "🥴".to_string()],
&[]
));
assert!(!check_word_mute_impl(
&texts,
&vec!["🙂".to_string(), "🥲".to_string(), "🥴".to_string()],
&vec![]
&["🙂".to_string(), "🥲".to_string(), "🥴".to_string()],
&[]
));
assert!(check_word_mute_impl(&texts, &vec![], &vec![emoji.clone()]));
assert!(check_word_mute_impl(&texts, &[], &[emoji.clone()]));
}
}

View File

@ -55,10 +55,12 @@ pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
{
let _ = MTX_GUARD.lock().await;
attempted = cache::get_one::<bool>(cache::Category::FetchUrl, url)?.is_some();
attempted = cache::get_one::<bool>(cache::Category::FetchUrl, url)
.await?
.is_some();
if !attempted {
cache::set_one(cache::Category::FetchUrl, url, &true, 10 * 60)?;
cache::set_one(cache::Category::FetchUrl, url, &true, 10 * 60).await?;
}
}
@ -138,7 +140,7 @@ mod unit_test {
let mp3_url = "https://firefish.dev/firefish/firefish/-/blob/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/sounds/aisha/1.mp3";
// delete caches in case you run this test multiple times
cache::delete_all(cache::Category::FetchUrl).unwrap();
cache::delete_all(cache::Category::FetchUrl).await.unwrap();
let png_size_1 = ImageSize {
width: 1024,
@ -207,7 +209,9 @@ mod unit_test {
let url = "https://firefish.dev/firefish/firefish/-/raw/5891a90f71a8b9d5ea99c683ade7e485c685d642/packages/backend/assets/splash.png";
// delete caches in case you run this test multiple times
cache::delete_one(cache::Category::FetchUrl, url).unwrap();
cache::delete_one(cache::Category::FetchUrl, url)
.await
.unwrap();
assert!(get_image_size_from_url(url).await.is_ok());
assert!(get_image_size_from_url(url).await.is_err());

View File

@ -46,7 +46,7 @@ async fn get_latest_version() -> Result<String, Error> {
#[crate::export]
pub async fn latest_version() -> Result<String, Error> {
let version: Option<String> =
cache::get_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL)?;
cache::get_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL).await?;
if let Some(v) = version {
tracing::trace!("use cached value: {}", v);
@ -61,7 +61,8 @@ pub async fn latest_version() -> Result<String, Error> {
UPSTREAM_PACKAGE_JSON_URL,
&fetched_version,
3 * 60 * 60,
)?;
)
.await?;
Ok(fetched_version)
}
}
@ -97,7 +98,9 @@ mod unit_test {
#[tokio::test]
async fn check_version() {
// delete caches in case you run this test multiple times
cache::delete_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL).unwrap();
cache::delete_one(cache::Category::FetchUrl, UPSTREAM_PACKAGE_JSON_URL)
.await
.unwrap();
// fetch from firefish.dev
validate_version(latest_version().await.unwrap());

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "targetUserId")]
pub target_user_id: String,
#[sea_orm(column_name = "reporterId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub token: String,
pub hash: String,
#[sea_orm(column_name = "userId")]
@ -21,7 +21,7 @@ pub struct Model {
#[sea_orm(column_name = "appId")]
pub app_id: Option<String>,
#[sea_orm(column_name = "lastUsedAt")]
pub last_used_at: Option<DateTime>,
pub last_used_at: Option<DateTimeWithTimeZone>,
pub session: Option<String>,
pub name: Option<String>,
pub description: Option<String>,

View File

@ -13,9 +13,9 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "expiresAt")]
pub expires_at: DateTime,
pub expires_at: DateTimeWithTimeZone,
pub place: String,
pub priority: String,
pub url: String,

View File

@ -13,13 +13,13 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub text: String,
pub title: String,
#[sea_orm(column_name = "imageUrl")]
pub image_url: Option<String>,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: Option<DateTime>,
pub updated_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "showPopup")]
pub show_popup: bool,
#[sea_orm(column_name = "isGoodNews")]

View File

@ -17,7 +17,7 @@ pub struct Model {
#[sea_orm(column_name = "announcementId")]
pub announcement_id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -14,7 +14,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub name: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: Option<String>,
pub secret: String,

View File

@ -16,7 +16,7 @@ pub struct Model {
pub user_id: String,
pub challenge: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "registrationChallenge")]
pub registration_challenge: bool,
}

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub token: String,
#[sea_orm(column_name = "userId")]
pub user_id: Option<String>,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "blockeeId")]
pub blockee_id: String,
#[sea_orm(column_name = "blockerId")]

View File

@ -13,9 +13,9 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "lastNotedAt")]
pub last_noted_at: Option<DateTime>,
pub last_noted_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "userId")]
pub user_id: Option<String>,
pub name: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "followeeId")]
pub followee_id: String,
#[sea_orm(column_name = "followerId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "channelId")]
pub channel_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub name: String,

View File

@ -14,7 +14,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: Option<String>,
#[sea_orm(column_name = "userHost")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub name: String,
#[sea_orm(column_name = "userId")]
pub user_id: Option<String>,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: Option<DateTime>,
pub updated_at: Option<DateTimeWithTimeZone>,
pub name: String,
pub host: Option<String>,
#[sea_orm(column_name = "originalUrl")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "followeeId")]
pub followee_id: String,
#[sea_orm(column_name = "followerId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "followeeId")]
pub followee_id: String,
#[sea_orm(column_name = "followerId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "postId")]

View File

@ -13,9 +13,9 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTime,
pub updated_at: DateTimeWithTimeZone,
pub title: String,
pub description: Option<String>,
#[sea_orm(column_name = "userId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "caughtAt")]
pub caught_at: DateTime,
pub caught_at: DateTimeWithTimeZone,
pub host: String,
#[sea_orm(column_name = "usersCount")]
pub users_count: i32,
@ -24,13 +24,13 @@ pub struct Model {
#[sea_orm(column_name = "followersCount")]
pub followers_count: i32,
#[sea_orm(column_name = "latestRequestSentAt")]
pub latest_request_sent_at: Option<DateTime>,
pub latest_request_sent_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "latestStatus")]
pub latest_status: Option<i32>,
#[sea_orm(column_name = "latestRequestReceivedAt")]
pub latest_request_received_at: Option<DateTime>,
pub latest_request_received_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "lastCommunicatedAt")]
pub last_communicated_at: DateTime,
pub last_communicated_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "isNotResponding")]
pub is_not_responding: bool,
#[sea_orm(column_name = "softwareName")]
@ -46,7 +46,7 @@ pub struct Model {
#[sea_orm(column_name = "maintainerEmail")]
pub maintainer_email: Option<String>,
#[sea_orm(column_name = "infoUpdatedAt")]
pub info_updated_at: Option<DateTime>,
pub info_updated_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "isSuspended")]
pub is_suspended: bool,
#[sea_orm(column_name = "iconUrl")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "recipientId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub r#type: String,

View File

@ -13,13 +13,13 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "muteeId")]
pub mutee_id: String,
#[sea_orm(column_name = "muterId")]
pub muter_id: String,
#[sea_orm(column_name = "expiresAt")]
pub expires_at: Option<DateTime>,
pub expires_at: Option<DateTimeWithTimeZone>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -14,7 +14,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "replyId")]
pub reply_id: Option<String>,
#[sea_orm(column_name = "renoteId")]
@ -66,7 +66,7 @@ pub struct Model {
#[sea_orm(column_name = "threadId")]
pub thread_id: Option<String>,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: Option<DateTime>,
pub updated_at: Option<DateTimeWithTimeZone>,
pub lang: Option<String>,
}

View File

@ -20,7 +20,7 @@ pub struct Model {
#[sea_orm(column_name = "fileIds")]
pub file_ids: Vec<String>,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTime,
pub updated_at: DateTimeWithTimeZone,
pub emojis: Vec<String>,
}

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "threadId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -14,7 +14,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "notifieeId")]
pub notifiee_id: String,
#[sea_orm(column_name = "notifierId")]

View File

@ -14,9 +14,9 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTime,
pub updated_at: DateTimeWithTimeZone,
pub title: String,
pub name: String,
pub summary: Option<String>,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "pageId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub token: String,
#[sea_orm(column_name = "userId")]
pub user_id: String,

View File

@ -14,7 +14,7 @@ pub struct Model {
#[sea_orm(column_name = "noteId", primary_key, auto_increment = false, unique)]
pub note_id: String,
#[sea_orm(column_name = "expiresAt")]
pub expires_at: Option<DateTime>,
pub expires_at: Option<DateTimeWithTimeZone>,
pub multiple: bool,
pub choices: Vec<String>,
pub votes: Vec<i32>,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(column_name = "noteId", primary_key, auto_increment = false, unique)]
pub note_id: String,
#[sea_orm(column_name = "expiresAt")]
pub expires_at: DateTime,
pub expires_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
}

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub code: String,
}

View File

@ -13,9 +13,9 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTime,
pub updated_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub key: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "muteeId")]
pub mutee_id: String,
#[sea_orm(column_name = "muterId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "muteeId")]
pub mutee_id: String,
#[sea_orm(column_name = "muterId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub ip: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub endpoint: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub username: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -14,11 +14,11 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: Option<DateTime>,
pub updated_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "lastFetchedAt")]
pub last_fetched_at: Option<DateTime>,
pub last_fetched_at: Option<DateTimeWithTimeZone>,
pub username: String,
#[sea_orm(column_name = "usernameLower")]
pub username_lower: String,
@ -62,7 +62,7 @@ pub struct Model {
#[sea_orm(column_name = "followersUri")]
pub followers_uri: Option<String>,
#[sea_orm(column_name = "lastActiveDate")]
pub last_active_date: Option<DateTime>,
pub last_active_date: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "hideOnlineStatus")]
pub hide_online_status: bool,
#[sea_orm(column_name = "isDeleted")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub name: String,
#[sea_orm(column_name = "userId")]
pub user_id: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "userGroupId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "userGroupId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "userGroupId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub ip: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub name: String,

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "userListId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "noteId")]

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
pub code: String,
pub username: String,
pub email: String,

View File

@ -17,7 +17,7 @@ pub struct Model {
#[sea_orm(column_name = "publicKey")]
pub public_key: String,
#[sea_orm(column_name = "lastUsed")]
pub last_used: DateTime,
pub last_used: DateTimeWithTimeZone,
pub name: String,
}

View File

@ -13,7 +13,7 @@ pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTime,
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
pub name: String,
@ -22,7 +22,7 @@ pub struct Model {
pub secret: String,
pub active: bool,
#[sea_orm(column_name = "latestSentAt")]
pub latest_sent_at: Option<DateTime>,
pub latest_sent_at: Option<DateTimeWithTimeZone>,
#[sea_orm(column_name = "latestStatus")]
pub latest_status: Option<i32>,
}

View File

@ -1,11 +1,11 @@
use crate::database::cache;
use crate::database::{db_conn, redis_conn, redis_key};
use crate::database::{db_conn, redis_conn, redis_key, RedisConnError};
use crate::federation::acct::Acct;
use crate::misc::check_hit_antenna::{check_hit_antenna, AntennaCheckError};
use crate::model::entity::{antenna, note};
use crate::service::stream;
use crate::util::id::{get_timestamp, InvalidIdErr};
use redis::{streams::StreamMaxlen, Commands, RedisError};
use redis::{streams::StreamMaxlen, AsyncCommands, RedisError};
use sea_orm::{DbErr, EntityTrait};
#[derive(thiserror::Error, Debug)]
@ -16,6 +16,8 @@ pub enum Error {
CacheErr(#[from] cache::Error),
#[error("Redis error: {0}")]
RedisErr(#[from] RedisError),
#[error("Redis connection error: {0}")]
RedisConnErr(#[from] RedisConnError),
#[error("Invalid ID: {0}")]
InvalidIdErr(#[from] InvalidIdErr),
#[error("Stream error: {0}")]
@ -33,9 +35,9 @@ type Note = note::Model;
async fn antennas() -> Result<Vec<Antenna>, Error> {
const CACHE_KEY: &str = "antennas";
Ok(cache::get::<Vec<Antenna>>(CACHE_KEY)?.unwrap_or({
Ok(cache::get::<Vec<Antenna>>(CACHE_KEY).await?.unwrap_or({
let antennas = antenna::Entity::find().all(db_conn().await?).await?;
cache::set(CACHE_KEY, &antennas, 5 * 60)?;
cache::set(CACHE_KEY, &antennas, 5 * 60).await?;
antennas
}))
}
@ -52,22 +54,25 @@ pub async fn update_antennas_on_new_note(
continue;
}
if check_hit_antenna(antenna, note.clone(), note_author).await? {
add_note_to_antenna(&antenna.id, &note)?;
add_note_to_antenna(&antenna.id, &note).await?;
}
}
Ok(())
}
pub fn add_note_to_antenna(antenna_id: &str, note: &Note) -> Result<(), Error> {
pub async fn add_note_to_antenna(antenna_id: &str, note: &Note) -> Result<(), Error> {
// for timeline API
redis_conn()?.xadd_maxlen(
redis_key(format!("antennaTimeline:{}", antenna_id)),
StreamMaxlen::Approx(200),
format!("{}-*", get_timestamp(&note.id)?),
&[("note", &note.id)],
)?;
redis_conn()
.await?
.xadd_maxlen(
redis_key(format!("antennaTimeline:{}", antenna_id)),
StreamMaxlen::Approx(200),
format!("{}-*", get_timestamp(&note.id)?),
&[("note", &note.id)],
)
.await?;
// for streaming API
Ok(stream::antenna::publish(antenna_id.to_string(), note)?)
Ok(stream::antenna::publish(antenna_id.to_string(), note).await?)
}

View File

@ -116,13 +116,13 @@ async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> {
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)?;
let cached = cache::get::<Nodeinfo21>(NODEINFO_2_1_CACHE_KEY).await?;
if let Some(nodeinfo) = cached {
Ok(nodeinfo)
} else {
let nodeinfo = generate_nodeinfo_2_1().await?;
cache::set(NODEINFO_2_1_CACHE_KEY, &nodeinfo, 60 * 60)?;
cache::set(NODEINFO_2_1_CACHE_KEY, &nodeinfo, 60 * 60).await?;
Ok(nodeinfo)
}
}

View File

@ -12,7 +12,7 @@ pub async fn watch_note(
if watcher_id != note_author_id {
note_watching::Entity::insert(note_watching::ActiveModel {
id: ActiveValue::set(gen_id()),
created_at: ActiveValue::set(chrono::Local::now().naive_local()),
created_at: ActiveValue::set(chrono::Utc::now().into()),
user_id: ActiveValue::Set(watcher_id.to_string()),
note_user_id: ActiveValue::Set(note_author_id.to_string()),
note_id: ActiveValue::Set(note_id.to_string()),

View File

@ -47,6 +47,7 @@ pub enum PushNotificationKind {
ReadNotifications,
#[strum(serialize = "readAllNotifications")]
ReadAllNotifications,
Mastodon,
}
fn compact_content(
@ -158,15 +159,29 @@ pub async fn send_push_notification(
.all(db)
.await?;
let payload = format!(
"{{\"type\":\"{}\",\"userId\":\"{}\",\"dateTime\":{},\"body\":{}}}",
kind,
receiver_user_id,
chrono::Utc::now().timestamp_millis(),
serde_json::to_string(&compact_content(&kind, content.clone())?)?
);
// TODO: refactoring
let payload = if kind == PushNotificationKind::Mastodon {
// Leave the `content` as it is
serde_json::to_string(content)?
} else {
// Format the `content` passed from the TypeScript backend
// for Firefish push notifications
format!(
"{{\"type\":\"{}\",\"userId\":\"{}\",\"dateTime\":{},\"body\":{}}}",
kind,
receiver_user_id,
chrono::Utc::now().timestamp_millis(),
serde_json::to_string(&compact_content(&kind, content.clone())?)?
)
};
tracing::trace!("payload: {:#?}", payload);
let encoding = if kind == PushNotificationKind::Mastodon {
ContentEncoding::AesGcm
} else {
ContentEncoding::Aes128Gcm
};
for subscription in subscriptions.iter() {
if !subscription.send_read_message
&& [
@ -211,7 +226,7 @@ pub async fn send_push_notification(
let mut message_builder = WebPushMessageBuilder::new(&subscription_info);
message_builder.set_ttl(1000);
message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes());
message_builder.set_payload(encoding, payload.as_bytes());
message_builder.set_vapid_signature(signature.unwrap());
let message = message_builder.build();

View File

@ -7,8 +7,8 @@ pub mod group_chat;
pub mod moderation;
use crate::config::CONFIG;
use crate::database::redis_conn;
use redis::{Commands, RedisError};
use crate::database::{redis_conn, RedisConnError};
use redis::{AsyncCommands, RedisError};
#[derive(strum::Display)]
pub enum Stream {
@ -49,13 +49,15 @@ pub enum Stream {
pub enum Error {
#[error("Redis error: {0}")]
RedisError(#[from] RedisError),
#[error("Redis connection error: {0}")]
RedisConnErr(#[from] RedisConnError),
#[error("Json (de)serialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Value error: {0}")]
ValueError(String),
}
pub fn publish_to_stream(
pub async fn publish_to_stream(
stream: &Stream,
kind: Option<String>,
value: Option<String>,
@ -70,10 +72,13 @@ pub fn publish_to_stream(
value.ok_or(Error::ValueError("Invalid streaming message".to_string()))?
};
redis_conn()?.publish(
&CONFIG.host,
format!("{{\"channel\":\"{}\",\"message\":{}}}", stream, message),
)?;
redis_conn()
.await?
.publish(
&CONFIG.host,
format!("{{\"channel\":\"{}\",\"message\":{}}}", stream, message),
)
.await?;
Ok(())
}

View File

@ -1,10 +1,11 @@
use crate::model::entity::note;
use crate::service::stream::{publish_to_stream, Error, Stream};
pub fn publish(antenna_id: String, note: &note::Model) -> Result<(), Error> {
pub async fn publish(antenna_id: String, note: &note::Model) -> Result<(), Error> {
publish_to_stream(
&Stream::Antenna { antenna_id },
Some("note".to_string()),
Some(serde_json::to_string(note)?),
)
.await
}

View File

@ -1,10 +1,11 @@
use crate::service::stream::{publish_to_stream, Error, Stream};
#[crate::export(js_name = "publishToChannelStream")]
pub fn publish(channel_id: String, user_id: String) -> Result<(), Error> {
pub async fn publish(channel_id: String, user_id: String) -> Result<(), Error> {
publish_to_stream(
&Stream::Channel { channel_id },
Some("typing".to_string()),
Some(format!("\"{}\"", user_id)),
)
.await
}

View File

@ -17,7 +17,7 @@ pub enum ChatEvent {
// https://github.com/napi-rs/napi-rs/issues/2036
#[crate::export(js_name = "publishToChatStream")]
pub fn publish(
pub async fn publish(
sender_user_id: String,
receiver_user_id: String,
kind: ChatEvent,
@ -31,4 +31,5 @@ pub fn publish(
Some(kind.to_string()),
Some(serde_json::to_string(object)?),
)
.await
}

View File

@ -13,7 +13,7 @@ pub enum ChatIndexEvent {
// https://github.com/napi-rs/napi-rs/issues/2036
#[crate::export(js_name = "publishToChatIndexStream")]
pub fn publish(
pub async fn publish(
user_id: String,
kind: ChatIndexEvent,
object: &serde_json::Value,
@ -23,4 +23,5 @@ pub fn publish(
Some(kind.to_string()),
Some(serde_json::to_string(object)?),
)
.await
}

View File

@ -18,10 +18,11 @@ pub struct PackedEmoji {
}
#[crate::export(js_name = "publishToBroadcastStream")]
pub fn publish(emoji: &PackedEmoji) -> Result<(), Error> {
pub async fn publish(emoji: &PackedEmoji) -> Result<(), Error> {
publish_to_stream(
&Stream::CustomEmoji,
Some("emojiAdded".to_string()),
Some(format!("{{\"emoji\":{}}}", serde_json::to_string(emoji)?)),
)
.await
}

View File

@ -4,10 +4,15 @@ use crate::service::stream::{chat::ChatEvent, publish_to_stream, Error, Stream};
// https://github.com/napi-rs/napi-rs/issues/2036
#[crate::export(js_name = "publishToGroupChatStream")]
pub fn publish(group_id: String, kind: ChatEvent, object: &serde_json::Value) -> Result<(), Error> {
pub async fn publish(
group_id: String,
kind: ChatEvent,
object: &serde_json::Value,
) -> Result<(), Error> {
publish_to_stream(
&Stream::GroupChat { group_id },
Some(kind.to_string()),
Some(serde_json::to_string(object)?),
)
.await
}

View File

@ -12,10 +12,11 @@ pub struct AbuseUserReportLike {
}
#[crate::export(js_name = "publishToModerationStream")]
pub fn publish(moderator_id: String, report: &AbuseUserReportLike) -> Result<(), Error> {
pub async fn publish(moderator_id: String, report: &AbuseUserReportLike) -> Result<(), Error> {
publish_to_stream(
&Stream::Moderation { moderator_id },
Some("newAbuseUserReport".to_string()),
Some(serde_json::to_string(report)?),
)
.await
}

View File

@ -40,11 +40,10 @@
"axios": "1.6.8",
"backend-rs": "workspace:*",
"blurhash": "2.0.5",
"bull": "4.12.5",
"bull": "4.12.6",
"cacheable-lookup": "TheEssem/cacheable-lookup",
"cbor-x": "1.5.9",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
@ -56,9 +55,9 @@
"feed": "4.2.2",
"file-type": "19.0.0",
"firefish-js": "workspace:*",
"fluent-ffmpeg": "2.1.2",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.2.1",
"got": "14.3.0",
"gunzip-maybe": "1.4.2",
"hpagent": "1.2.0",
"ioredis": "5.4.1",
@ -169,7 +168,7 @@
"@types/websocket": "1.0.10",
"@types/ws": "8.5.10",
"cross-env": "7.0.3",
"eslint": "9.2.0",
"eslint": "9.3.0",
"mocha": "10.4.0",
"pug": "3.0.2",
"strict-event-emitter-types": "2.0.0",

View File

@ -0,0 +1 @@
type DateTimeWithTimeZone = Date;

View File

@ -1,15 +1,11 @@
import * as fs from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import * as os from "node:os";
import cluster from "node:cluster";
import chalk from "chalk";
import chalkTemplate from "chalk-template";
import semver from "semver";
import Logger from "@/services/logger.js";
import {
fetchMeta,
greet,
initializeRustLogger,
removeOldAttestationChallenges,
showServerInfo,
@ -19,74 +15,9 @@ import { config, envOption } from "@/config.js";
import { db, initDb } from "@/db/postgre.js";
import { inspect } from "node:util";
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const meta = JSON.parse(
fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
);
const logger = new Logger("core", "cyan");
const bootLogger = logger.createSubLogger("boot", "magenta", false);
const themeColor = chalk.hex("#31748f");
function greet() {
//#region Firefish logo
console.log(
themeColor(
"██████╗ ██╗██████╗ ███████╗███████╗██╗███████╗██╗ ██╗ ○ ▄ ▄ ",
),
);
console.log(
themeColor(
"██╔════╝██║██╔══██╗██╔════╝██╔════╝██║██╔════╝██║ ██║ ⚬ █▄▄ █▄▄ ",
),
);
console.log(
themeColor(
"█████╗ ██║██████╔╝█████╗ █████╗ ██║███████╗███████║ ▄▄▄▄▄▄ ▄ ",
),
);
console.log(
themeColor(
"██╔══╝ ██║██╔══██╗██╔══╝ ██╔══╝ ██║╚════██║██╔══██║ █ █ █▄▄ ",
),
);
console.log(
themeColor(
"██║ ██║██║ ██║███████╗██║ ██║███████║██║ ██║ █ ● ● █ ",
),
);
console.log(
themeColor(
"╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ▀▄▄▄▄▄▄▀ ",
),
);
//#endregion
console.log(
" Firefish is an open-source decentralized microblogging platform.",
);
console.log(
chalk.rgb(
255,
136,
0,
)(
" If you like Firefish, please consider contributing to the repo. https://firefish.dev/firefish/firefish",
),
);
console.log("");
console.log(
chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
);
bootLogger.info("Welcome to Firefish!");
bootLogger.info(`Firefish v${meta.version}`, null, true);
}
/**
* Init master process
*/

View File

@ -0,0 +1,459 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class AddBackTimezone1715351290096 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "abuse_user_report" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "access_token" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "access_token" ALTER "lastUsedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "ad" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "ad" ALTER "expiresAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "announcement" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "announcement" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "announcement_read" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "app" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "attestation_challenge" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "auth_session" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "blocking" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel" ALTER "lastNotedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel_following" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel_note_pining" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "clip" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "drive_file" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "drive_folder" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "emoji" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "following" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "follow_request" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "gallery_like" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "gallery_post" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "gallery_post" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "caughtAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "infoUpdatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "lastCommunicatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "latestRequestReceivedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "latestRequestSentAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "messaging_message" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "moderation_log" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "muting" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "muting" ALTER "expiresAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_edit" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_favorite" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_reaction" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_thread_muting" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_watching" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "notification" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "page" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "page" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "page_like" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "password_reset_request" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "poll" ALTER "expiresAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "poll_vote" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "promo_note" ALTER "expiresAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "promo_read" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "registration_ticket" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "registry_item" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "registry_item" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "renote_muting" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "reply_muting" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "signin" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "sw_subscription" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "used_username" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "lastActiveDate" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "lastFetchedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "updatedAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group_invitation" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group_invite" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group_joining" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_ip" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_list" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_list_joining" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_note_pining" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_pending" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_security_key" ALTER "lastUsed" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "webhook" ALTER "createdAt" TYPE timestamp with time zone`,
);
await queryRunner.query(
`ALTER TABLE "webhook" ALTER "latestSentAt" TYPE timestamp with time zone`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "abuse_user_report" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "access_token" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "access_token" ALTER "lastUsedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "ad" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "ad" ALTER "expiresAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "announcement" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "announcement" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "announcement_read" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "app" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "attestation_challenge" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "auth_session" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "blocking" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel" ALTER "lastNotedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel_following" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "channel_note_pining" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "clip" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "drive_file" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "drive_folder" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "emoji" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "following" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "follow_request" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "gallery_like" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "gallery_post" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "gallery_post" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "caughtAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "infoUpdatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "lastCommunicatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "latestRequestReceivedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "instance" ALTER "latestRequestSentAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "messaging_message" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "moderation_log" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "muting" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "muting" ALTER "expiresAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_edit" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_favorite" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_reaction" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_thread_muting" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "note_watching" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "notification" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "page" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "page" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "page_like" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "password_reset_request" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "poll" ALTER "expiresAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "poll_vote" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "promo_note" ALTER "expiresAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "promo_read" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "registration_ticket" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "registry_item" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "registry_item" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "renote_muting" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "reply_muting" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "signin" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "sw_subscription" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "used_username" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "lastActiveDate" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "lastFetchedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER "updatedAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group_invitation" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group_invite" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_group_joining" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_ip" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_list" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_list_joining" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_note_pining" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_pending" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "user_security_key" ALTER "lastUsed" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "webhook" ALTER "createdAt" TYPE timestamp without time zone`,
);
await queryRunner.query(
`ALTER TABLE "webhook" ALTER "latestSentAt" TYPE timestamp without time zone`,
);
}
}

View File

@ -16,7 +16,7 @@ export class AbuseUserReport {
public id: string;
@Index()
@Column("timestamp without time zone", {
@Column("timestamp with time zone", {
comment: "The created date of the AbuseUserReport.",
})
public createdAt: Date;

View File

@ -16,12 +16,12 @@ export class AccessToken {
@PrimaryColumn(id())
public id: string;
@Column("timestamp without time zone", {
@Column("timestamp with time zone", {
comment: "The created date of the AccessToken.",
})
public createdAt: Date;
@Column("timestamp without time zone", {
@Column("timestamp with time zone", {
nullable: true,
})
public lastUsedAt: Date | null;

View File

@ -7,13 +7,13 @@ export class Ad {
public id: string;
@Index()
@Column("timestamp without time zone", {
@Column("timestamp with time zone", {
comment: "The created date of the Ad.",
})
public createdAt: Date;
@Index()
@Column("timestamp without time zone", {
@Column("timestamp with time zone", {
comment: "The expired date of the Ad.",
})
public expiresAt: Date;

Some files were not shown because too many files have changed in this diff Show More