Merge branch 'develop' into 'main'
release: v20240714 Co-authored-by: GitLab CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Linerly <linerly@proton.me> Co-authored-by: jolupa <jolupameister@gmail.com> Co-authored-by: Eana Hufwe <eana@1a23.com> Co-authored-by: mester yui <oscarodriguez56@gmail.com> See merge request firefish/firefish!11146
This commit is contained in:
commit
784c5724a1
285 changed files with 12476 additions and 11948 deletions
.gitignore.gitlab-ci.yml
.gitlab/issue_templates
Cargo.lockCargo.tomlDockerfileREADME.mdbiome.jsondocs
locales
package.jsonpackages
README.md
backend-rs
index.d.tsindex.jspackage.json
src
config
database
federation
init
misc
check_server_block.rscheck_word_mute.rsescape_sql.rsget_image_size.rsmastodon_id.rsmod.rs
note
reaction.rssystem_info.rsuser
model/entity
service
util
backend
package.json
src
migration
1709395223612-swSubscriptionAccessToken.ts1711075007936-userProfileMentions.ts1712425488543-drop-time-zone.ts1713108561474-clientCredentials.ts1715181461692-addMastodonSubscriptionType.ts1715351290096-add-back-timezone.ts1720618854585-create-system-actors.ts
misc
models
prelude
remote
server
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -57,9 +57,6 @@ packages/backend/assets/instance.css
|
|||
packages/backend/assets/sounds/None.mp3
|
||||
packages/backend/assets/LICENSE
|
||||
|
||||
packages/megalodon/lib
|
||||
packages/megalodon/.idea
|
||||
|
||||
dev/container/firefish
|
||||
dev/container/db
|
||||
dev/container/redis
|
||||
|
|
|
@ -30,6 +30,7 @@ stages:
|
|||
- build
|
||||
- dependency
|
||||
- clean
|
||||
- manage
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: 'firefish_db'
|
||||
|
@ -108,7 +109,6 @@ test:build:backend_ts:
|
|||
paths:
|
||||
- packages/backend/**/*
|
||||
- packages/firefish-js/**/*
|
||||
- packages/megalodon/**/*
|
||||
when: always
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
|
@ -128,7 +128,7 @@ test:build:backend_ts:
|
|||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command 'CREATE EXTENSION pgroonga'
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'backend' --filter 'firefish-js' --filter 'megalodon' run build:debug
|
||||
- pnpm --filter 'backend' --filter 'firefish-js' run build:debug
|
||||
- pnpm run migrate
|
||||
- psql --host postgres --user "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" --command "$(cat docs/downgrade.sql)"
|
||||
|
||||
|
@ -175,29 +175,7 @@ build:container:
|
|||
image: docker.io/debian:trixie-slim
|
||||
services: []
|
||||
rules:
|
||||
- if: $BUILD == 'true'
|
||||
when: always
|
||||
- if: $BUILD == 'false'
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH == 'develop'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- locales/**/*
|
||||
- scripts/copy-assets.mjs
|
||||
- package.json
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
when: always
|
||||
needs:
|
||||
- job: test:build
|
||||
optional: true
|
||||
- job: test:build:backend_ts_only
|
||||
optional: true
|
||||
- job: test:build:client_only
|
||||
optional: true
|
||||
- if: $BUILD && $CI_PIPELINE_SOURCE == 'schedule'
|
||||
variables:
|
||||
STORAGE_DRIVER: overlay
|
||||
before_script:
|
||||
|
@ -206,8 +184,8 @@ build:container:
|
|||
sed -i -r 's/"version": "([-0-9]+)",/"version": "\1-dev",/' package.json
|
||||
- apt-get install -y --no-install-recommends ca-certificates fuse-overlayfs buildah
|
||||
- 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"
|
||||
- export IMAGE_TAG_1="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
|
||||
- export IMAGE_TAG_2="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production-$(date +%Y%m%d)"
|
||||
- buildah version
|
||||
script:
|
||||
- |-
|
||||
|
@ -218,13 +196,12 @@ build:container:
|
|||
--security-opt apparmor=unconfined \
|
||||
--cap-add all \
|
||||
--platform linux/amd64 \
|
||||
--layers \
|
||||
--cache-to "${IMAGE_CACHE}" \
|
||||
--cache-from "${IMAGE_CACHE}" \
|
||||
--tag "${IMAGE_TAG}" \
|
||||
--tag "${IMAGE_TAG_1}" \
|
||||
--tag "${IMAGE_TAG_2}" \
|
||||
.
|
||||
- buildah inspect "${IMAGE_TAG}"
|
||||
- buildah push "${IMAGE_TAG}"
|
||||
- buildah inspect "${IMAGE_TAG_1}"
|
||||
- buildah push "${IMAGE_TAG_1}"
|
||||
- buildah push "${IMAGE_TAG_2}"
|
||||
|
||||
cargo:check:msrv:
|
||||
stage: test
|
||||
|
@ -363,3 +340,27 @@ clean:
|
|||
- pnpm install --frozen-lockfile
|
||||
script:
|
||||
- pnpm run clean-all
|
||||
|
||||
add-issue-labels:
|
||||
stage: manage
|
||||
rules:
|
||||
- if: $ADD_LABEL && $CI_PIPELINE_SOURCE == 'schedule'
|
||||
image: registry.firefish.dev/firefish/gitlab-issue-labels
|
||||
variables:
|
||||
GITLAB_HOST: "firefish.dev"
|
||||
services: []
|
||||
before_script: []
|
||||
script:
|
||||
- gitlab-issue-labels
|
||||
|
||||
close-stalled-issues:
|
||||
stage: manage
|
||||
rules:
|
||||
- if: $CLOSE_STALLED && $CI_PIPELINE_SOURCE == 'schedule'
|
||||
image: registry.firefish.dev/firefish/gitlab-issue-labels
|
||||
variables:
|
||||
GITLAB_HOST: "firefish.dev"
|
||||
services: []
|
||||
before_script: []
|
||||
script:
|
||||
- gitlab-issue-labels
|
||||
|
|
|
@ -16,16 +16,19 @@
|
|||
<!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
|
||||
|
||||
<!-- Uncomment (remove surrounding arrow signs) the following line(s) to specify the category of this issue. -->
|
||||
<!-- /label Server -->
|
||||
<!-- /label Client -->
|
||||
<!-- /label Mobile -->
|
||||
<!-- /label Third-party-client -->
|
||||
<!-- /label Docs -->
|
||||
<!-- /label Locale -->
|
||||
<!-- /label "Build from source" -->
|
||||
<!-- /label Container -->
|
||||
<!-- /label "Firefish API" -->
|
||||
<!-- /label "Mastodon API" -->
|
||||
<!-- * label: Server -->
|
||||
<!-- * label: Client -->
|
||||
<!-- * label: Mobile -->
|
||||
<!-- * label: Third-party-client -->
|
||||
<!-- * label: Docs -->
|
||||
<!-- * label: Locale -->
|
||||
<!-- * label: Build from source -->
|
||||
<!-- * label: Container -->
|
||||
<!-- * label: Firefish API -->
|
||||
<!-- * label: Mastodon API -->
|
||||
|
||||
<!-- Please do not edit the next line -->
|
||||
* label: Bug
|
||||
|
||||
## What happened?
|
||||
<!-- Please give us a brief description of what happened. -->
|
||||
|
@ -43,7 +46,7 @@
|
|||
<!-- Is it always reproducible, or is it conditional/probabilistic ? -->
|
||||
|
||||
|
||||
## What did you try to solve the issue
|
||||
## What did you try to solve the issue / Do you have any insights
|
||||
<!-- Not to repeat the same thing, let us share what you have tried so far. -->
|
||||
|
||||
|
||||
|
@ -81,8 +84,7 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
|
||||
|
||||
## Are you willing to fix this bug? (optional)
|
||||
<!-- Please uncomment the following line if you want to fix this bug -->
|
||||
<!-- /assign me -->
|
||||
- [ ] Yes, I will open a merge request that closes this ticket.
|
||||
|
||||
<!--
|
||||
Please tell us how to fix this bug.
|
||||
|
@ -95,8 +97,3 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
Many thanks for your involvement!
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Do not edit the following line -->
|
||||
/label Bug?
|
||||
|
|
|
@ -16,16 +16,19 @@
|
|||
<!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
|
||||
|
||||
<!-- Uncomment (remove surrounding arrow signs) the following line(s) to specify the category of this issue. -->
|
||||
<!-- /label Server -->
|
||||
<!-- /label Client -->
|
||||
<!-- /label Mobile -->
|
||||
<!-- /label Third-party-client -->
|
||||
<!-- /label Docs -->
|
||||
<!-- /label Locale -->
|
||||
<!-- /label "Build from source" -->
|
||||
<!-- /label Container -->
|
||||
<!-- /label "Firefish API" -->
|
||||
<!-- /label "Mastodon API" -->
|
||||
<!-- * label: Server -->
|
||||
<!-- * label: Client -->
|
||||
<!-- * label: Mobile -->
|
||||
<!-- * label: Third-party-client -->
|
||||
<!-- * label: Docs -->
|
||||
<!-- * label: Locale -->
|
||||
<!-- * label: Build from source -->
|
||||
<!-- * label: Container -->
|
||||
<!-- * label: Firefish API -->
|
||||
<!-- * label: Mastodon API -->
|
||||
|
||||
<!-- Please do not edit the next line -->
|
||||
* label: Discussion
|
||||
|
||||
## What do you think needs to be discussed?
|
||||
<!-- Please tell us your idea. -->
|
||||
|
@ -64,8 +67,7 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
|
||||
|
||||
## Are you willing to open a merge request? (optional)
|
||||
<!-- Please uncomment the following line if you want to implement this yourself -->
|
||||
<!-- /assign me -->
|
||||
- [ ] Yes, I will open a merge request that closes this ticket.
|
||||
|
||||
<!--
|
||||
Please tell us how do you want to implement your idea.
|
||||
|
@ -78,8 +80,3 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
Many thanks for your involvement!
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Do not edit the following line -->
|
||||
/label Discussion
|
||||
|
|
|
@ -16,16 +16,19 @@
|
|||
<!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
|
||||
|
||||
<!-- Uncomment (remove surrounding arrow signs) the following line(s) to specify the category of this issue. -->
|
||||
<!-- /label Server -->
|
||||
<!-- /label Client -->
|
||||
<!-- /label Mobile -->
|
||||
<!-- /label Third-party-client -->
|
||||
<!-- /label Docs -->
|
||||
<!-- /label Locale -->
|
||||
<!-- /label "Build from source" -->
|
||||
<!-- /label Container -->
|
||||
<!-- /label "Firefish API" -->
|
||||
<!-- /label "Mastodon API" -->
|
||||
<!-- * label: Server -->
|
||||
<!-- * label: Client -->
|
||||
<!-- * label: Mobile -->
|
||||
<!-- * label: Third-party-client -->
|
||||
<!-- * label: Docs -->
|
||||
<!-- * label: Locale -->
|
||||
<!-- * label: Build from source -->
|
||||
<!-- * label: Container -->
|
||||
<!-- * label: Firefish API -->
|
||||
<!-- * label: Mastodon API -->
|
||||
|
||||
<!-- Please do not edit the next line -->
|
||||
* label: Feature
|
||||
|
||||
## What feature would you like implemented?
|
||||
<!-- Please give us a brief description of what you'd like to be refactored. -->
|
||||
|
@ -49,8 +52,7 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
|
||||
|
||||
## Are you willing to implement this feature? (optional)
|
||||
<!-- Please uncomment the following line if you want to implement this feature -->
|
||||
<!-- /assign me -->
|
||||
- [ ] Yes, I will open a merge request that closes this ticket.
|
||||
|
||||
<!--
|
||||
Please tell us how to implement this feature.
|
||||
|
@ -63,8 +65,3 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
Many thanks for your involvement!
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Do not edit the following line -->
|
||||
/label Feature
|
||||
|
|
|
@ -16,16 +16,19 @@
|
|||
<!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
|
||||
|
||||
<!-- Uncomment (remove surrounding arrow signs) the following line(s) to specify the category of this issue. -->
|
||||
<!-- /label Server -->
|
||||
<!-- /label Client -->
|
||||
<!-- /label Mobile -->
|
||||
<!-- /label Third-party-client -->
|
||||
<!-- /label Docs -->
|
||||
<!-- /label Locale -->
|
||||
<!-- /label "Build from source" -->
|
||||
<!-- /label Container -->
|
||||
<!-- /label "Firefish API" -->
|
||||
<!-- /label "Mastodon API" -->
|
||||
<!-- * label: Server -->
|
||||
<!-- * label: Client -->
|
||||
<!-- * label: Mobile -->
|
||||
<!-- * label: Third-party-client -->
|
||||
<!-- * label: Docs -->
|
||||
<!-- * label: Locale -->
|
||||
<!-- * label: Build from source -->
|
||||
<!-- * label: Container -->
|
||||
<!-- * label: Firefish API -->
|
||||
<!-- * label: Mastodon API -->
|
||||
|
||||
<!-- Please do not edit the next line -->
|
||||
* label: Refactor
|
||||
|
||||
## What parts of the code do you think should be refactored?
|
||||
<!-- Please give us a brief description of what you'd like. -->
|
||||
|
@ -49,8 +52,7 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
|
||||
|
||||
## Are you willing to refactor the code? (optional)
|
||||
<!-- Please uncomment the following line if you want to implement it -->
|
||||
<!-- /assign me -->
|
||||
- [ ] Yes, I will open a merge request that closes this ticket.
|
||||
|
||||
<!--
|
||||
Please tell us how to refactor the code.
|
||||
|
@ -63,8 +65,3 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
|
|||
Many thanks for your involvement!
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Do not edit the following line -->
|
||||
/label Refactor
|
||||
|
|
102
Cargo.lock
generated
102
Cargo.lock
generated
|
@ -92,7 +92,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -143,7 +143,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -154,7 +154,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -410,9 +410,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.105"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5208975e568d83b6b05cc0a063c8e7e9acc2b43bee6da15616a5b73e109d7437"
|
||||
checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
|
@ -629,7 +629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -772,7 +772,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1388,7 +1388,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1467,7 +1467,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1496,7 +1496,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1744,7 +1744,7 @@ dependencies = [
|
|||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1840,7 +1840,7 @@ dependencies = [
|
|||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1855,7 +1855,7 @@ dependencies = [
|
|||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1885,9 +1885,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nom-exif"
|
||||
version = "1.2.0"
|
||||
version = "1.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4da85c96824dc1b583f55fd2c39e53b04482b4c199f25db27868db334bf06407"
|
||||
checksum = "3dac386e49252f313c88764101ca16681caa4a5b0260d494d58b6af7f04edd3a"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"nom",
|
||||
|
@ -1984,7 +1984,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2076,7 +2076,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2137,7 +2137,7 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2295,7 +2295,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2468,7 +2468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2664,9 +2664,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.42"
|
||||
version = "0.8.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eeba50c58624afb3be6d04abad8cb7a259d52017068c9f828975aa870a5daf5"
|
||||
checksum = "1aee83dc281d5a3200d37b299acd13b81066ea126a7f16f0eae70fc9aed241d9"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
@ -2839,7 +2839,7 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2878,7 +2878,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"sea-bae",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
|
@ -2955,7 +2955,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3406,9 +3406,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.69"
|
||||
version = "2.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6"
|
||||
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -3435,14 +3435,14 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.12"
|
||||
version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae"
|
||||
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
|
@ -3467,9 +3467,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.14"
|
||||
version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||
checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
|
@ -3485,22 +3485,22 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3566,9 +3566,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22"
|
||||
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
@ -3605,7 +3605,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3668,9 +3668,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.14"
|
||||
version = "0.22.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
|
@ -3699,7 +3699,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3827,9 +3827,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.9.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -3911,7 +3911,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
@ -3933,7 +3933,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
@ -4214,7 +4214,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
"synstructure 0.13.1",
|
||||
]
|
||||
|
||||
|
@ -4235,7 +4235,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4255,7 +4255,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
"synstructure 0.13.1",
|
||||
]
|
||||
|
||||
|
@ -4284,7 +4284,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.69",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -26,7 +26,7 @@ emojis = { version = "0.6.2", default-features = false }
|
|||
idna = { version = "1.0.2", default-features = false }
|
||||
image = { version = "0.25.1", default-features = false }
|
||||
isahc = { version = "1.7.2", default-features = false }
|
||||
nom-exif = { version = "1.2.0", default-features = false }
|
||||
nom-exif = { version = "1.2.6", default-features = false }
|
||||
once_cell = { version = "1.19.0", default-features = false }
|
||||
pretty_assertions = { version = "1.4.0", default-features = false }
|
||||
proc-macro2 = { version = "1.0.86", default-features = false }
|
||||
|
@ -39,9 +39,9 @@ sea-orm = { version = "0.12.15", default-features = false }
|
|||
serde = { version = "1.0.204", default-features = false }
|
||||
serde_json = { version = "1.0.120", default-features = false }
|
||||
serde_yaml = { version = "0.9.34", default-features = false }
|
||||
syn = { version = "2.0.69", default-features = false }
|
||||
sysinfo = { version = "0.30.12", default-features = false }
|
||||
thiserror = { version = "1.0.61", default-features = false }
|
||||
syn = { version = "2.0.71", default-features = false }
|
||||
sysinfo = { version = "0.30.13", default-features = false }
|
||||
thiserror = { version = "1.0.62", default-features = false }
|
||||
tokio = { version = "1.38.0", default-features = false }
|
||||
tokio-test = { version = "0.4.4", default-features = false }
|
||||
tracing = { version = "0.1.40", default-features = false }
|
||||
|
|
|
@ -36,7 +36,6 @@ COPY packages/backend/package.json packages/backend/package.json
|
|||
COPY packages/client/package.json packages/client/package.json
|
||||
COPY packages/sw/package.json packages/sw/package.json
|
||||
COPY packages/firefish-js/package.json packages/firefish-js/package.json
|
||||
COPY packages/megalodon/package.json packages/megalodon/package.json
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Install dev mode dependencies for compilation
|
||||
|
@ -60,8 +59,6 @@ RUN apk update && apk add --no-cache zip unzip tini ffmpeg curl
|
|||
|
||||
COPY . ./
|
||||
|
||||
COPY --from=build /firefish/packages/megalodon /firefish/packages/megalodon
|
||||
|
||||
# Copy node modules
|
||||
COPY --from=build /firefish/node_modules /firefish/node_modules
|
||||
COPY --from=build /firefish/packages/backend/node_modules /firefish/packages/backend/node_modules
|
||||
|
|
|
@ -17,13 +17,13 @@ Firefish is based off of Misskey, a powerful microblogging server on ActivityPub
|
|||
|
||||
# Documents
|
||||
|
||||
- [Installation guide](./docs/install.md)
|
||||
- [Installation guide](https://firefish.dev/firefish/firefish/-/blob/main/docs/install.md)
|
||||
- [Contributing guide](./CONTRIBUTING.md)
|
||||
- [Changelog](./docs/changelog.md)
|
||||
- [Changelog](https://firefish.dev/firefish/firefish/-/blob/main/docs/changelog.md)
|
||||
|
||||
# Links
|
||||
|
||||
- Donations: <https://opencollective.com/Firefish>
|
||||
- Donations: <https://opencollective.com/firefish>
|
||||
- Matrix space: <https://matrix.to/#/#firefish-community:nitro.chat>
|
||||
- Official account: <a href="https://info.firefish.dev/@firefish" rel="me">`@firefish@info.firefish.dev`</a>
|
||||
- Weblate: <https://hosted.weblate.org/engage/firefish/>
|
||||
|
@ -31,6 +31,6 @@ Firefish is based off of Misskey, a powerful microblogging server on ActivityPub
|
|||
# Want to get involved? Great!
|
||||
|
||||
- If you know how to program in TypeScript, Vue, or Rust, please read the [contributing guide](./CONTRIBUTING.md).
|
||||
- If you have the means to, [donations](https://opencollective.com/Firefish) are a great way to keep us going.
|
||||
- If you have the means to, [donations](https://opencollective.com/firefish) are a great way to keep us going.
|
||||
- If you know a non-English language, translating Firefish on [Weblate](https://hosted.weblate.org/engage/firefish/) help bring Firefish to more people. No technical experience needed!
|
||||
|
||||
|
|
15
biome.json
15
biome.json
|
@ -45,7 +45,6 @@
|
|||
"style": {
|
||||
"noCommaOperator": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noNamespace": "error",
|
||||
"noNonNullAssertion": "warn",
|
||||
"noUselessElse": "off",
|
||||
"noVar": "error",
|
||||
|
@ -398,6 +397,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["packages/backend/src/server/api/mastodon/**/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"noParameterAssign": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off",
|
||||
"noThisInStatic": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,6 +2,137 @@
|
|||
|
||||
Breaking changes are indicated by the :warning: icon.
|
||||
|
||||
## v20240714
|
||||
|
||||
- The old Mastodon API has been replaced with a new implementation based on Iceshrimp’s.
|
||||
- :warning: The new API uses a new format to manage Mastodon sessions in the database, whereas old implementation uses Misskey sessions. All previous client app and token registrations will not work with the new API. All clients need to be re-registered and all users need to re-authenticate.
|
||||
- :warning: All IDs (of statuses/notes, notifications, users, etc.) will be using the alphanumerical format, aligning with the Firefish/Misskey API. The old numerical IDs will not work when queried against the new API.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Available endpoints (under <code>https://instance-domain/api/</code>)</summary>
|
||||
|
||||
| method | endpoint | note |
|
||||
|----------|------------------------------------|--------------------------------------------|
|
||||
| `POST` | `oauth/token` | |
|
||||
| `POST` | `oauth/revoke` | |
|
||||
| `POST` | `v1/apps` | |
|
||||
| `GET` | `v1/apps/verify_credentials` | |
|
||||
| `POST` | `v1/firefish/apps/info` | Firefish extension, uses MiAuth |
|
||||
| `POST` | `v1/firefish/auth/code` | Firefish extension, uses MiAuth |
|
||||
| | | |
|
||||
| `GET` | `v1/accounts/verify_credentials` | |
|
||||
| `PATCH` | `v1/accounts/update_credentials` | |
|
||||
| `GET` | `v1/accounts/lookup` | |
|
||||
| `GET` | `v1/accounts/relationships` | |
|
||||
| `GET` | `v1/accounts/search` | |
|
||||
| `GET` | `v1/accounts/:id` | |
|
||||
| `GET` | `v1/accounts/:id/statuses` | |
|
||||
| `GET` | `v1/accounts/:id/featured_tags` | |
|
||||
| `GET` | `v1/accounts/:id/followers` | |
|
||||
| `GET` | `v1/accounts/:id/following` | |
|
||||
| `GET` | `v1/accounts/:id/lists` | |
|
||||
| `POST` | `v1/accounts/:id/follow` | |
|
||||
| `POST` | `v1/accounts/:id/unfollow` | |
|
||||
| `POST` | `v1/accounts/:id/block` | |
|
||||
| `POST` | `v1/accounts/:id/unblock` | |
|
||||
| `POST` | `v1/accounts/:id/mute` | |
|
||||
| `POST` | `v1/accounts/:id/unmute` | |
|
||||
| | | |
|
||||
| `GET` | `v1/featured_tags` | always returns an empty list |
|
||||
| `GET` | `v1/followed_tags` | always returns an empty list |
|
||||
| `GET` | `v1/bookmarks` | |
|
||||
| `GET` | `v1/favourites` | |
|
||||
| | | |
|
||||
| `GET` | `v1/mutes` | |
|
||||
| `GET` | `v1/blocks` | |
|
||||
| `GET` | `v1/follow_requests` | |
|
||||
| `POST` | `v1/follow_requests/:id/authorize` | |
|
||||
| `POST` | `v1/follow_requests/:id/reject` | |
|
||||
| | | |
|
||||
| `GET` | `v1/filters` | |
|
||||
| `POST` | `v1/filters` | |
|
||||
| `GET` | `v2/filters` | |
|
||||
| `POST` | `v2/filters` | |
|
||||
| | | |
|
||||
| `GET` | `v1/lists` | |
|
||||
| `POST` | `v1/lists` | |
|
||||
| `GET` | `v1/lists/:id` | |
|
||||
| `PUT` | `v1/lists/:id` | |
|
||||
| `DELETE` | `v1/lists/:id` | |
|
||||
| `GET` | `v1/lists/:id/accounts` | |
|
||||
| `POST` | `v1/lists/:id/accounts` | |
|
||||
| `DELETE` | `v1/lists/:id/accounts` | |
|
||||
| | | |
|
||||
| `GET` | `v1/media/:id` | |
|
||||
| `PUT` | `v1/media/:id` | |
|
||||
| `POST` | `v1/media` | |
|
||||
| `POST` | `v2/media` | |
|
||||
| | | |
|
||||
| `GET` | `v1/custom_emojis` | |
|
||||
| `GET` | `v1/instance` | |
|
||||
| `GET` | `v2/instance` | |
|
||||
| `GET` | `v1/announcements` | |
|
||||
| `POST` | `v1/announcements/:id/dismiss` | |
|
||||
| `GET` | `v1/trends` | pagination is unimplemented |
|
||||
| `GET` | `v1/trends/tags` | pagination is unimplemented |
|
||||
| `GET` | `v1/trends/statuses` | |
|
||||
| `GET` | `v1/trends/links` | always returns an empty list |
|
||||
| `GET` | `v1/preferences` | |
|
||||
| `GET` | `v2/suggestions` | |
|
||||
| | | |
|
||||
| `GET` | `v1/notifications` | |
|
||||
| `GET` | `v1/notifications/:id` | |
|
||||
| `POST` | `v1/notifications/clear` | |
|
||||
| `POST` | `v1/notifications/:id/dismiss` | |
|
||||
| `POST` | `v1/conversations/:id/read` | |
|
||||
| `GET` | `v1/push/subscription` | |
|
||||
| `POST` | `v1/push/subscription` | |
|
||||
| `DELETE` | `v1/push/subscription` | |
|
||||
| | | |
|
||||
| `GET` | `v1/search` | |
|
||||
| `GET` | `v2/search` | |
|
||||
| | | |
|
||||
| `POST` | `v1/statuses` | |
|
||||
| `PUT` | `v1/statuses/:id` | |
|
||||
| `GET` | `v1/statuses/:id` | |
|
||||
| `DELETE` | `v1/statuses/:id` | |
|
||||
| `GET` | `v1/statuses/:id/context` | |
|
||||
| `GET` | `v1/statuses/:id/history` | |
|
||||
| `GET` | `v1/statuses/:id/source` | |
|
||||
| `GET` | `v1/statuses/:id/reblogged_by` | |
|
||||
| `GET` | `v1/statuses/:id/favourited_by` | |
|
||||
| `POST` | `v1/statuses/:id/favourite` | |
|
||||
| `POST` | `v1/statuses/:id/unfavourite` | |
|
||||
| `POST` | `v1/statuses/:id/reblog` | |
|
||||
| `POST` | `v1/statuses/:id/unreblog` | |
|
||||
| `POST` | `v1/statuses/:id/bookmark` | |
|
||||
| `POST` | `v1/statuses/:id/unbookmark` | |
|
||||
| `POST` | `v1/statuses/:id/pin` | |
|
||||
| `POST` | `v1/statuses/:id/unpin` | |
|
||||
| `POST` | `v1/statuses/:id/react/:name` | |
|
||||
| `POST` | `v1/statuses/:id/unreact/:name` | |
|
||||
| `POST` | `v1/statuses/:id/translate` | |
|
||||
| | | |
|
||||
| `GET` | `v1/polls/:id` | |
|
||||
| `POST` | `v1/polls/:id/votes` | |
|
||||
| | | |
|
||||
| `GET` | `v1/scheduled_statuses` | |
|
||||
| `GET` | `v1/scheduled_statuses/:id` | reschedule (`PUT` method) is unimplemented |
|
||||
| `DELETE` | `v1/scheduled_statuses/:id` | |
|
||||
| | | |
|
||||
| `GET` | `v1/streaming/health` | |
|
||||
| | | |
|
||||
| `GET` | `v1/timelines/public` | |
|
||||
| `GET` | `v1/timelines/tag/:hashtag` | |
|
||||
| `GET` | `v1/timelines/home` | |
|
||||
| `GET` | `v1/timelines/list/:listId` | |
|
||||
| `GET` | `v1/conversations` | |
|
||||
| `GET` | `v1/markers` | |
|
||||
| `POST` | `v1/markers` | |
|
||||
|
||||
</details>
|
||||
|
||||
## v20240710
|
||||
|
||||
- Added `readCatLanguage` field to the response of `i` and request of `i/update` (optional).
|
||||
|
|
|
@ -2,8 +2,28 @@
|
|||
|
||||
Critical security updates are indicated by the :warning: icon.
|
||||
|
||||
- Server administrators must 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.
|
||||
- Server administrators must check [notice-for-admins.md](https://firefish.dev/firefish/firefish/-/blob/main/docs/notice-for-admins.md) as well.
|
||||
- Third-party client/bot developers may want to check [api-change.md](https://firefish.dev/firefish/firefish/-/blob/main/docs/api-change.md) as well.
|
||||
|
||||
## [v20240714](https://firefish.dev/firefish/firefish/-/merge_requests/11146/commits)
|
||||
|
||||
- Mastodon API implementation was ported from Iceshrimp, with added Firefish extensions including push notifications, post languages, schedule post support, and more. (#10880)
|
||||
- Fix bugs
|
||||
|
||||
### Acknowledgement
|
||||
|
||||
The new Mastodon API support would not have been possible without the significant dedication of Laura Hausmann (Iceshrimp lead developer). We thank her and other Iceshrimp contributors from the bottom of our hearts.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- The new Mastodon API uses a new format to manage Mastodon sessions in the database, whereas old implementation uses Misskey sessions. All previous client app and token registrations will not work with the new API. All clients need to be re-registered and all users need to re-authenticate.
|
||||
- All IDs (of statuses/notes, notifications, users, etc.) will be using the alphanumerical format, aligning with the Firefish/Misskey API. The old numerical IDs will not work when queried against the new API.
|
||||
|
||||
### Important Notice
|
||||
|
||||
The new Mastodon API support still contains some incompatibilities and unimplemented features, so please keep in mind that you may experience glitchy behavior, and please do NOT report such issues to Mastodon client apps. Such a “bug” is likely due to our implementation, and Mastodon client developers should not be bothered by such an invalid bug report. In the worst scenario, they may simply block non-Mastodon implementations (some clients already do that).
|
||||
|
||||
If you find an incompatibility issue (a bug not reproducible with a vanilla Mastodon server), file it to the Firefish repository instead. However, please remember that it is impossible to achieve 100% compatibility, given that Mastodon servers don’t behave exactly like its own documentation.
|
||||
|
||||
## [v20240710](https://firefish.dev/firefish/firefish/-/merge_requests/11110/commits)
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'CreateSystemActors1720618854585',
|
||||
'AddMastodonSubscriptionType1715181461692',
|
||||
'SwSubscriptionAccessToken1709395223611',
|
||||
'UserProfileMentions1711075007936',
|
||||
'ClientCredentials1713108561474',
|
||||
'TurnOffCatLanguage1720107645050',
|
||||
'RefactorScheduledPosts1716804636187',
|
||||
'RemoveEnumTypenameSuffix1716462794927',
|
||||
|
@ -35,6 +40,20 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
'RemoveNativeUtilsMigration1705877093218'
|
||||
);
|
||||
|
||||
-- addMastodonSubscriptionType
|
||||
ALTER TABLE "sw_subscription" DROP COLUMN "subscriptionTypes";
|
||||
DROP TYPE "push_subscription_type";
|
||||
|
||||
-- sw-subscription-per-access-token
|
||||
ALTER TABLE "sw_subscription" DROP CONSTRAINT "FK_98a1aa2db2a5253924f42f38767";
|
||||
ALTER TABLE "sw_subscription" DROP COLUMN "appAccessTokenId";
|
||||
|
||||
-- user-profile-mentions
|
||||
ALTER TABLE "user_profile" DROP COLUMN "mentions";
|
||||
|
||||
-- client-credential-support
|
||||
ALTER TABLE "access_token" ALTER COLUMN "userId" SET NOT NULL;
|
||||
|
||||
-- turn-off-cat-language
|
||||
ALTER TABLE "user" DROP COLUMN "readCatLanguage";
|
||||
|
||||
|
|
|
@ -51,4 +51,4 @@ NOTE: This will take some time to come fully online, even after download and ext
|
|||
|
||||
Once the server is up you can use a web browser to access the web interface at `http://serverip:3000` (where `serverip` is the IP of the server you are running the firefish server on).
|
||||
|
||||
To publish your server, please follow the instructions in [section 5 of this installation guide](./install.md#5-preparation-for-publishing-a-server).
|
||||
To publish your server, please follow the instructions in [section 5 of this installation guide](https://firefish.dev/firefish/firefish/-/blob/main/docs/install.md#5-preparation-for-publishing-a-server).
|
||||
|
|
|
@ -35,7 +35,7 @@ We don't test Firefish on non-Linux systems, so please install Firefish on such
|
|||
|
||||
<details>
|
||||
|
||||
<summary>Possible setup on FreeBSD (as of version `20240630`)</summary>
|
||||
<summary>Possible setup on FreeBSD (as of version <code>20240630</code>)</summary>
|
||||
|
||||
You can install Firefish on FreeBSD by adding these extra steps to the standard instructions:
|
||||
|
||||
|
@ -365,7 +365,7 @@ In this instruction, we use [Caddy](https://caddyserver.com/) to make the Firefi
|
|||
|
||||
## Upgrade Firefish version
|
||||
|
||||
Please refer to the [upgrade instruction](./upgrade.md). Be sure to switch to `firefish` user and go to the Firefish directory before executing the `git` command:
|
||||
Please refer to the [upgrade instruction](https://firefish.dev/firefish/firefish/-/blob/main/docs/upgrade.md). Be sure to switch to `firefish` user and go to the Firefish directory before executing the `git` command:
|
||||
|
||||
```sh
|
||||
sudo su --login firefish
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
# Notice for server administrators
|
||||
|
||||
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).
|
||||
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](https://firefish.dev/firefish/firefish/-/blob/main/docs/upgrade.md).
|
||||
|
||||
## Upcoming breaking change (unreleased)
|
||||
|
||||
Please take a look at #10947.
|
||||
|
||||
## v20240714
|
||||
|
||||
### For systemd/pm2 users
|
||||
|
||||
You can remove the `packages/megalodon` directory after pulling the latest source code (`git pull --ff origin main`).
|
||||
|
||||
```sh
|
||||
rm --recursive --force packages/megalodon
|
||||
```
|
||||
|
||||
## v20240710
|
||||
|
||||
### For all users
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## For systemd/pm2 users
|
||||
|
||||
1. Check [`docs/notice-for-admins.md`](./notice-for-admins.md)
|
||||
1. Check [`docs/notice-for-admins.md`](https://firefish.dev/firefish/firefish/-/blob/main/docs/notice-for-admins.md)
|
||||
1. Stop the server
|
||||
```sh
|
||||
sudo systemctl stop your-firefish-service.service
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
## For Docker/Podman users
|
||||
|
||||
1. Check [`docs/notice-for-admins.md`](./notice-for-admins.md)
|
||||
1. Check [`docs/notice-for-admins.md`](https://firefish.dev/firefish/firefish/-/blob/main/docs/notice-for-admins.md)
|
||||
1. Pull the latest container image
|
||||
```sh
|
||||
docker pull registry.firefish.dev/firefish/firefish:latest
|
||||
|
|
|
@ -2324,3 +2324,4 @@ addAlt4MeTag: "Afegeix automàticament l'etiqueta #Alt4Me a les teves publicacio
|
|||
que tinguin un fitxer adjunt sense descripció"
|
||||
strongPassword: Bona contrasenya
|
||||
turnOffCatLanguage: Desactiva la conversió al llenguatge de gat
|
||||
announcement: Anunci
|
||||
|
|
|
@ -293,6 +293,7 @@ usernameOrUserId: "Username or user id"
|
|||
noSuchUser: "User not found"
|
||||
lookup: "Lookup"
|
||||
announcements: "Announcements"
|
||||
announcement: "Announcement"
|
||||
imageUrl: "Image URL"
|
||||
remove: "Delete"
|
||||
removed: "Successfully deleted"
|
||||
|
|
|
@ -2189,3 +2189,9 @@ noSentFollowRequests: No has enviado ninguna solicitud de seguimiento
|
|||
antennaLimit: El número máximo de antenas que cada usuario puede crear
|
||||
toEdit: Editar
|
||||
cannotEditVisibility: No puedes editar la visibilidad
|
||||
i18nServerChange: Use el {language} en su lugar.
|
||||
i18nServerSet: Use el {language} para nuevos clientes.
|
||||
squareCatAvatars: Mostrar avatares cuadrados para las cuentas de gatos
|
||||
useThisAccountConfirm: ¿Quieres continuar con esta cuenta?
|
||||
i18nServerInfo: Nuevos clientes estarán en {language} por defecto.
|
||||
media: Medios
|
||||
|
|
|
@ -463,7 +463,7 @@ usernameInvalidFormat: "Hanya dapat menerima karakter a-z, A-Z dan angka 0-9."
|
|||
tooShort: "Terlalu pendek"
|
||||
tooLong: "Terlalu panjang"
|
||||
weakPassword: "Kata sandi lemah"
|
||||
normalPassword: "Kata sandi baik"
|
||||
normalPassword: "Kata sandi sedang"
|
||||
veryStrongPassword: "Kata sandi kuat"
|
||||
passwordMatched: "Kata sandi sama"
|
||||
passwordNotMatched: "Kata sandi tidak sama"
|
||||
|
@ -1120,7 +1120,7 @@ _wordMute:
|
|||
regular expressions."
|
||||
softDescription: "Sembunyikan postingan yang memenuhi aturan kondisi dari lini masa."
|
||||
hardDescription: "Cegah postingan memenuhi aturan kondisi dari ditambahkan ke lini
|
||||
masa. Dengan tambahan, kiriman berikut tidak akan ditambahkan ke lini masa meskipun
|
||||
masa. Dengan tambahan, postingan berikut tidak akan ditambahkan ke lini masa meskipun
|
||||
jika kondisi tersebut diubah."
|
||||
soft: "Lembut"
|
||||
hard: "Keras"
|
||||
|
@ -1223,8 +1223,8 @@ _sfx:
|
|||
antenna: "Penerimaan Antenna"
|
||||
channel: "Pemberitahuan saluran"
|
||||
_ago:
|
||||
future: "Masa depan"
|
||||
justNow: "Baru saja"
|
||||
future: "masa depan"
|
||||
justNow: "baru saja"
|
||||
secondsAgo: "{n} detik lalu"
|
||||
minutesAgo: "{n} menit lalu"
|
||||
hoursAgo: "{n} jam lalu"
|
||||
|
@ -1255,7 +1255,7 @@ _tutorial:
|
|||
step3_4: "Bingung tidak berpikiran untuk mengatakan sesuatu? Coba saja \"baru aja
|
||||
ikutan bikin akun misskey punyaku\"!"
|
||||
step4_1: "Mari kita lihat kamu di sana."
|
||||
step4_2: "Untuk kiriman pertama kamu, beberapa orang biasanya membuat postingan
|
||||
step4_2: "Untuk postingan pertama kamu, beberapa orang biasanya membuat postingan
|
||||
{introduction} atau \"Halo dunia!\" yang sederhana."
|
||||
step5_1: "Linimasa, linimasa di mana-mana!"
|
||||
step5_2: "Servermu memiliki {timelines} lini masa berbeda yang diaktifkan."
|
||||
|
@ -1890,7 +1890,7 @@ recommended: Direkomendasikan
|
|||
silenceThisInstance: Bisukan server ini
|
||||
hiddenTags: Tagar Tersembunyi
|
||||
preferencesBackups: Preferensi cadangan
|
||||
editNote: Sunting kiriman
|
||||
editNote: Sunting postingan
|
||||
deleted: Dihapus
|
||||
edited: Disunting pada {date} {time}
|
||||
selectInstance: Pilih server
|
||||
|
@ -2148,12 +2148,12 @@ confirm: Konfirmasi
|
|||
importZip: Impor ZIP
|
||||
exportZip: Ekspor ZIP
|
||||
detectPostLanguage: Deteksi bahasa secara otomatis dan tampilkan tombol terjemahkan
|
||||
untuk kiriman dalam bahasa asing
|
||||
indexableDescription: Perbolehkan pencarian di sini untuk menampilkan kiriman publikmu
|
||||
untuk postingan dalam bahasa asing
|
||||
indexableDescription: Perbolehkan pencarian di sini untuk menampilkan postingan publikmu
|
||||
indexable: Dapat diindeks
|
||||
languageForTranslation: Bahasa terjemahan kiriman
|
||||
languageForTranslation: Bahasa terjemahan postingan
|
||||
openServerInfo: Tampilkan informasi server dengan mengeklik ticker server di sebuah
|
||||
kiriman
|
||||
postingan
|
||||
vibrate: Putar getaran
|
||||
clickToShowPatterns: Klik untuk menampilkan pola modul
|
||||
iconSet: Set ikon
|
||||
|
@ -2199,52 +2199,52 @@ forMobile: Ponsel
|
|||
replaceChatButtonWithAccountButton: Ganti tombol percakapan dengan tombol ganti akun
|
||||
replaceWidgetsButtonWithReloadButton: Ganti tombol widget dengan tombol muat ulang
|
||||
searchEngine: Mesin pencarian yang digunakan dalam bilah pencarian MFM
|
||||
postSearch: Pencarian kiriman di server ini
|
||||
showBigPostButton: Tampilkan tombol kirim besar di formulir kiriman
|
||||
showPreviewByDefault: Tampilkan pratinjau dalam formulir kiriman secara bawaan
|
||||
postSearch: Pencarian postingan di server ini
|
||||
showBigPostButton: Tampilkan tombol kirim besar di formulir postingan
|
||||
showPreviewByDefault: Tampilkan pratinjau dalam formulir postingan secara bawaan
|
||||
useCdn: Dapatkan aset dari CDN
|
||||
useCdnDescription: Muat aset statis seperti Twemoji dari CDN JSDelivr daripada server
|
||||
Firefish ini.
|
||||
sentFollowRequests: Permintaan mengikuti terkirim
|
||||
replyUnmute: Suarakan balasan dalam lini masa
|
||||
noSentFollowRequests: Kamu belum punya permintaan mengikuti
|
||||
noSentFollowRequests: Kamu belum mengirim permintaan mengikuti
|
||||
enablePullToRefresh: Aktifkan "Tarik ke bawah untuk memuat ulang"
|
||||
pullDownToReload: Tarik ke bawah untuk memuat ulang
|
||||
enableTimelineStreaming: Perbarui lini masa secara otomatis
|
||||
searchWords: Kata-kata untuk dicari / ID atau URL untuk dicari
|
||||
searchUsers: Dikirim oleh (opsional)
|
||||
searchCwAndAlt: Termasuk peringatan konten dan deskripsi berkas
|
||||
searchPostsWithFiles: Hanya kiriman dengan berkas
|
||||
searchPostsWithFiles: Hanya postingan dengan berkas
|
||||
publishTimelines: Terbitkan lini masa untuk pengunjung
|
||||
publishTimelinesDescription: Jika diaktifkan, lini masa Lokal dan Global akan ditampilkan
|
||||
di {url} bahkan ketika keluar dari akun.
|
||||
searchWordsDescription: "Masukkan kata kunci di sini untuk mencari kiriman. Pisahkan
|
||||
searchWordsDescription: "Masukkan kata kunci di sini untuk mencari postingan. Pisahkan
|
||||
kata dengan spasi untuk pencarian AND (dan), atau 'OR' ('atau', tanpa tanda kutip)
|
||||
di antara kata-kata untuk pencarian OR.\nMisalnya, 'pagi malam' akan menemukan postingan
|
||||
yang mengandung 'pagi' dan 'malam', dan 'pagi OR malam' akan menemukan postingan
|
||||
yang mengandung 'pagi' atau 'malam' (atau keduanya).\nAnda juga dapat menggabungkan
|
||||
kondisi AND/OR seperti '(pagi OR malam) mengantuk'.\n\nJika kamu ingin membuka halaman
|
||||
pengguna atau halaman postingan tertentu, masukkan ID atau URL pada kolom ini dan
|
||||
klik tombol 'Cari'. Mengklik 'Cari' akan mencari postingan yang secara harfiah mengandung
|
||||
ID/URL."
|
||||
kondisi AND/OR seperti '(pagi OR malam) mengantuk'.\n\n Jika kamu ingin membuka
|
||||
halaman pengguna atau halaman postingan tertentu, masukkan ID atau URL pada kolom
|
||||
ini dan klik tombol 'Cari'. Mengeklik 'Cari' akan mencari postingan yang secara
|
||||
harfiah mengandung ID/URL."
|
||||
pullToRefreshThreshold: Jarak penarikan untuk memuat ulang
|
||||
releaseToReload: Lepaskan untuk memuat ulang
|
||||
reloading: Memuat ulang
|
||||
replyMute: Bisukan balasan dalam lini masa
|
||||
searchRange: Dikirim dalam (opsional)
|
||||
searchUsersDescription: "Untuk mencari kiriman oleh pengguna/server tertentu, masukkan
|
||||
searchUsersDescription: "Untuk mencari postingan oleh pengguna/server tertentu, masukkan
|
||||
ID (@pengguna@contoh.id, atau @pengguna untuk pengguna lokal) atau nama domain (contoh.id)\n
|
||||
\nJika kamu memasukkan 'me' ('aku', tanpa tanda kutip), semua kirimanmu (termasuk
|
||||
kiriman yang tidak terdaftar, khusus pengikut, langsung, dan rahasia) akan dicari.\n
|
||||
\nJika kamu memasukkan 'me' ('aku', tanpa tanda kutip), semua postinganmu (termasuk
|
||||
postingan yang tidak terdaftar, khusus pengikut, langsung, dan rahasia) akan dicari.\n
|
||||
\nJika Anda memasukkan 'local' (tanpa tanda kutip), hasilnya akan disaring untuk
|
||||
menyertakan hanya kiriman dari server ini."
|
||||
menyertakan hanya postingan dari server ini."
|
||||
searchRangeDescription: "Jika kamu ingin memfilter periode waktu, masukkan dalam format
|
||||
ini: 20220615-20231031\n\nJika kamu menghilangkan tahun (seperti 0105-0106 atau
|
||||
20231105-0110), maka akan ditafsirkan sebagai tahun saat ini.\n\nKamu juga bisa
|
||||
menghilangkan tanggal awal atau akhir. Sebagai contoh, -0102 akan memfilter hasil
|
||||
pencarian untuk menampilkan hanya kiriman yang dibuat sebelum tanggal 2 Januari
|
||||
pencarian untuk menampilkan hanya postingan yang dibuat sebelum tanggal 2 Januari
|
||||
tahun ini, dan 20231026- akan memfilter hasil pencarian untuk menampilkan hanya
|
||||
kiriman yang dibuat setelah tanggal 26 Oktober 2023."
|
||||
postingan yang dibuat setelah tanggal 26 Oktober 2023."
|
||||
toPost: Kirim
|
||||
toQuote: Kutip
|
||||
noAltTextWarning: Beberapa berkas yang dilampirkan tidak memiliki deskripsi. Lupa
|
||||
|
@ -2258,14 +2258,14 @@ messagingUnencryptedInfo: Percakapan di Firefish tidak terenkripsi secara ujung
|
|||
moderationNote: Catatan Moderasi
|
||||
driveCapacityOverride: Penimpaan Kapasitas Drive
|
||||
ipFirstAcknowledged: Tanggal akuisisi pertama dari alamat IP
|
||||
incorrectLanguageWarning: "Sepertinya kirimanmu dalam bahasa {detected}, tetapi Anda
|
||||
memilih {current}.\nApakah kamu ingin ubah bahasanya ke bahasa {detected} saja?"
|
||||
autocorrectNoteLanguage: Tampilkan peringatan jika bahasa kiriman tidak cocok dengan
|
||||
incorrectLanguageWarning: "Sepertinya postinganmu dalam bahasa {detected}, tetapi
|
||||
Anda memilih {current}.\nApakah kamu ingin ubah bahasanya ke bahasa {detected} saja?"
|
||||
autocorrectNoteLanguage: Tampilkan peringatan jika bahasa postingan tidak cocok dengan
|
||||
hasil yang dideteksi secara otomatis
|
||||
markLocalFilesNsfwByDefault: Tandai semua berkas lokal baru sensitif secara bawaan
|
||||
markLocalFilesNsfwByDefaultDescription: Terlepas dari pengaturan ini, pengguna dapat
|
||||
menghapus sendiri tanda NSFW. Berkas yang ada tidak berpengaruh.
|
||||
noteEditHistory: Riwayat penyuntingan kiriman
|
||||
noteEditHistory: Riwayat penyuntingan postingan
|
||||
media: Media
|
||||
antennaLimit: Jumlah antena maksimum yang dapat dibuat oleh setiap pengguna
|
||||
showAddFileDescriptionAtFirstPost: Buka formulir secara otomatis untuk menulis deskripsi
|
||||
|
@ -2278,3 +2278,28 @@ useThisAccountConfirm: Apakah kamu ingin melanjutkan dengan akun ini?
|
|||
inputAccountId: Silakan memasukkan akunmu (misalnya, @firefish@info.firefish.dev)
|
||||
copyRemoteFollowUrl: Salin URL ikuti jarak jauh
|
||||
slashQuote: Kutipan rantai
|
||||
scheduledPostAt: Kiriman akan dikirim {time}
|
||||
scheduledPost: Jadwalkan postingan ini
|
||||
_later:
|
||||
monthsAgo: dalam {n}bln
|
||||
secondsAgo: dalam {n}d
|
||||
future: masa depan
|
||||
justNow: saat ini
|
||||
daysAgo: dalam {n}hr
|
||||
hoursAgo: dalam {n}j
|
||||
minutesAgo: dalam {n}m
|
||||
weeksAgo: dalam {m}mg
|
||||
yearsAgo: dalam {n}thn
|
||||
mergeRenotesInTimeline: Kelompokkan beberapa pembagian postingan yang sama
|
||||
cancelScheduledPost: Hapus jadwal
|
||||
scheduledDate: Tanggal terjadwal
|
||||
mergeThreadInTimeline: Gabungkan beberapa postingan dalam utas yang sama dalam lini
|
||||
masa
|
||||
strongPassword: Kata sandi baik
|
||||
announcement: Pengumuman
|
||||
i18nServerSet: Gunakan {language} untuk klien baru.
|
||||
turnOffCatLanguage: Matikan konversi bahasa kucing
|
||||
i18nServerInfo: Klien baru akan dalam {language} secara bawaan.
|
||||
i18nServerChange: Gunakan {language} saja.
|
||||
addAlt4MeTag: 'Tambahkan tagar #Alt4Me secara otomatis pada postinganmu jika berkas
|
||||
yang dilampirkan tidak ada deskripsi'
|
||||
|
|
|
@ -254,6 +254,7 @@ usernameOrUserId: "ユーザー名かユーザーID"
|
|||
noSuchUser: "ユーザーが見つかりません"
|
||||
lookup: "照会"
|
||||
announcements: "お知らせ"
|
||||
announcement: "お知らせ"
|
||||
imageUrl: "画像URL"
|
||||
remove: "削除"
|
||||
removed: "削除しました"
|
||||
|
|
|
@ -243,6 +243,7 @@ usernameOrUserId: "用户名或用户 ID"
|
|||
noSuchUser: "用户不存在"
|
||||
lookup: "查询"
|
||||
announcements: "公告"
|
||||
announcement: "公告"
|
||||
imageUrl: "图片 URL"
|
||||
remove: "删除"
|
||||
removed: "已删除"
|
||||
|
|
|
@ -242,6 +242,7 @@ usernameOrUserId: "使用者名稱或使用者ID"
|
|||
noSuchUser: "使用者不存在"
|
||||
lookup: "查詢"
|
||||
announcements: "公告"
|
||||
announcement: "公告"
|
||||
imageUrl: "圖片URL"
|
||||
remove: "刪除"
|
||||
removed: "已成功刪除"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "firefish",
|
||||
"version": "20240710",
|
||||
"version": "20240714",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://firefish.dev/firefish/firefish.git"
|
||||
|
|
|
@ -8,4 +8,3 @@ This directory contains all of the packages Firefish uses.
|
|||
- `client`: Web interface written in Vue3 and TypeScript
|
||||
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
|
||||
- `firefish-js`: TypeScript SDK for both backend and client
|
||||
- `megalodon`: TypeScript library used for partial Mastodon API compatibility
|
||||
|
|
29
packages/backend-rs/index.d.ts
vendored
29
packages/backend-rs/index.d.ts
vendored
|
@ -30,7 +30,7 @@ export interface AccessToken {
|
|||
createdAt: DateTimeWithTimeZone
|
||||
token: string
|
||||
hash: string
|
||||
userId: string
|
||||
userId: string | null
|
||||
appId: string | null
|
||||
lastUsedAt: DateTimeWithTimeZone | null
|
||||
session: string | null
|
||||
|
@ -257,6 +257,8 @@ export interface Config {
|
|||
userAgent: string
|
||||
}
|
||||
|
||||
export declare function countLocalUsers(): Promise<number>
|
||||
|
||||
export declare function countReactions(reactions: Record<string, number>): Record<string, number>
|
||||
|
||||
export interface Cpu {
|
||||
|
@ -417,8 +419,6 @@ export interface FollowRequest {
|
|||
/** Converts milliseconds to a human readable string. */
|
||||
export declare function formatMilliseconds(milliseconds: number): string
|
||||
|
||||
export declare function fromMastodonId(mastodonId: string): string | null
|
||||
|
||||
export interface GalleryLike {
|
||||
id: string
|
||||
createdAt: DateTimeWithTimeZone
|
||||
|
@ -461,6 +461,8 @@ export declare function getFullApAccount(username: string, host?: string | undef
|
|||
|
||||
export declare function getImageSizeFromUrl(url: string): Promise<ImageSize>
|
||||
|
||||
export declare function getInternalActor(actor: InternalActor): Promise<User>
|
||||
|
||||
export declare function getNoteSummary(fileIds: Array<string>, text: string | undefined | null, cw: string | undefined | null, hasPoll: boolean): string
|
||||
|
||||
export declare function getTimestamp(id: string): number
|
||||
|
@ -542,6 +544,9 @@ export interface Instance {
|
|||
faviconUrl: string | null
|
||||
}
|
||||
|
||||
export type InternalActor = 'instance'|
|
||||
'relay';
|
||||
|
||||
/**
|
||||
* Checks if a server is allowlisted.
|
||||
* Returns `Ok(true)` if private mode is disabled.
|
||||
|
@ -1149,6 +1154,17 @@ export type PushNotificationKind = 'generic'|
|
|||
'readAllNotifications'|
|
||||
'mastodon';
|
||||
|
||||
export type PushSubscriptionType = 'adminReport'|
|
||||
'adminSignUp'|
|
||||
'favourite'|
|
||||
'follow'|
|
||||
'followRequest'|
|
||||
'mention'|
|
||||
'poll'|
|
||||
'reblog'|
|
||||
'status'|
|
||||
'update';
|
||||
|
||||
export interface RedisConfig {
|
||||
host: string
|
||||
port: number
|
||||
|
@ -1294,6 +1310,8 @@ export interface Software20 {
|
|||
/** Escapes `%` and `\` in the given string. */
|
||||
export declare function sqlLikeEscape(src: string): string
|
||||
|
||||
export declare function sqlRegexEscape(src: string): string
|
||||
|
||||
export interface Storage {
|
||||
/** Total storage space in bytes */
|
||||
total: number
|
||||
|
@ -1313,6 +1331,8 @@ export interface SwSubscription {
|
|||
auth: string
|
||||
publickey: string
|
||||
sendReadMessage: boolean
|
||||
appAccessTokenId: string | null
|
||||
subscriptionTypes: Array<PushSubscriptionType>
|
||||
}
|
||||
|
||||
export interface SysLogConfig {
|
||||
|
@ -1327,8 +1347,6 @@ export interface TlsConfig {
|
|||
|
||||
export declare function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
|
||||
|
||||
export declare function toMastodonId(firefishId: string): string | null
|
||||
|
||||
export declare function toPuny(host: string): string
|
||||
|
||||
export declare function unwatchNote(watcherId: string, noteId: string): Promise<void>
|
||||
|
@ -1509,6 +1527,7 @@ export interface UserProfile {
|
|||
preventAiLearning: boolean
|
||||
isIndexable: boolean
|
||||
mutedPatterns: Array<string>
|
||||
mentions: Json
|
||||
mutedInstances: Array<string>
|
||||
mutedWords: Array<string>
|
||||
lang: string | null
|
||||
|
|
|
@ -366,6 +366,7 @@ module.exports.AntennaSrc = nativeBinding.AntennaSrc
|
|||
module.exports.ChatEvent = nativeBinding.ChatEvent
|
||||
module.exports.ChatIndexEvent = nativeBinding.ChatIndexEvent
|
||||
module.exports.checkWordMute = nativeBinding.checkWordMute
|
||||
module.exports.countLocalUsers = nativeBinding.countLocalUsers
|
||||
module.exports.countReactions = nativeBinding.countReactions
|
||||
module.exports.cpuInfo = nativeBinding.cpuInfo
|
||||
module.exports.cpuUsage = nativeBinding.cpuUsage
|
||||
|
@ -379,13 +380,13 @@ module.exports.fetchMeta = nativeBinding.fetchMeta
|
|||
module.exports.fetchNodeinfo = nativeBinding.fetchNodeinfo
|
||||
module.exports.FILE_TYPE_BROWSERSAFE = nativeBinding.FILE_TYPE_BROWSERSAFE
|
||||
module.exports.formatMilliseconds = nativeBinding.formatMilliseconds
|
||||
module.exports.fromMastodonId = nativeBinding.fromMastodonId
|
||||
module.exports.generateSecureRandomString = nativeBinding.generateSecureRandomString
|
||||
module.exports.generateUserToken = nativeBinding.generateUserToken
|
||||
module.exports.genId = nativeBinding.genId
|
||||
module.exports.genIdAt = nativeBinding.genIdAt
|
||||
module.exports.getFullApAccount = nativeBinding.getFullApAccount
|
||||
module.exports.getImageSizeFromUrl = nativeBinding.getImageSizeFromUrl
|
||||
module.exports.getInternalActor = nativeBinding.getInternalActor
|
||||
module.exports.getNoteSummary = nativeBinding.getNoteSummary
|
||||
module.exports.getTimestamp = nativeBinding.getTimestamp
|
||||
module.exports.greet = nativeBinding.greet
|
||||
|
@ -393,6 +394,7 @@ module.exports.hashPassword = nativeBinding.hashPassword
|
|||
module.exports.HOUR = nativeBinding.HOUR
|
||||
module.exports.Inbound = nativeBinding.Inbound
|
||||
module.exports.initializeRustLogger = nativeBinding.initializeRustLogger
|
||||
module.exports.InternalActor = nativeBinding.InternalActor
|
||||
module.exports.isAllowedServer = nativeBinding.isAllowedServer
|
||||
module.exports.isBlockedServer = nativeBinding.isBlockedServer
|
||||
module.exports.isOldPasswordAlgorithm = nativeBinding.isOldPasswordAlgorithm
|
||||
|
@ -427,6 +429,7 @@ module.exports.publishToGroupChatStream = nativeBinding.publishToGroupChatStream
|
|||
module.exports.publishToModerationStream = nativeBinding.publishToModerationStream
|
||||
module.exports.publishToNotesStream = nativeBinding.publishToNotesStream
|
||||
module.exports.PushNotificationKind = nativeBinding.PushNotificationKind
|
||||
module.exports.PushSubscriptionType = nativeBinding.PushSubscriptionType
|
||||
module.exports.RelayStatus = nativeBinding.RelayStatus
|
||||
module.exports.removeOldAttestationChallenges = nativeBinding.removeOldAttestationChallenges
|
||||
module.exports.safeForSql = nativeBinding.safeForSql
|
||||
|
@ -435,10 +438,10 @@ module.exports.sendPushNotification = nativeBinding.sendPushNotification
|
|||
module.exports.shouldNyaify = nativeBinding.shouldNyaify
|
||||
module.exports.showServerInfo = nativeBinding.showServerInfo
|
||||
module.exports.sqlLikeEscape = nativeBinding.sqlLikeEscape
|
||||
module.exports.sqlRegexEscape = nativeBinding.sqlRegexEscape
|
||||
module.exports.storageUsage = nativeBinding.storageUsage
|
||||
module.exports.stringToAcct = nativeBinding.stringToAcct
|
||||
module.exports.toDbReaction = nativeBinding.toDbReaction
|
||||
module.exports.toMastodonId = nativeBinding.toMastodonId
|
||||
module.exports.toPuny = nativeBinding.toPuny
|
||||
module.exports.unwatchNote = nativeBinding.unwatchNote
|
||||
module.exports.updateAntennaCache = nativeBinding.updateAntennaCache
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"binaryName": "backend-rs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.56"
|
||||
"@napi-rs/cli": "3.0.0-alpha.58"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --features napi --no-const-enum --platform --release --output-dir ./built/",
|
||||
|
|
|
@ -346,7 +346,7 @@ pub fn load_config() -> Config {
|
|||
hostname,
|
||||
redis_key_prefix,
|
||||
scheme,
|
||||
ws_scheme: ws_scheme.to_string(),
|
||||
ws_scheme: ws_scheme.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ fn wildcard(category: Category) -> String {
|
|||
/// # use backend_rs::database::cache;
|
||||
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key = "apple";
|
||||
/// let data = "I want to cache this string".to_string();
|
||||
/// let data = "I want to cache this string".to_owned();
|
||||
///
|
||||
/// // caches the data for 10 seconds
|
||||
/// cache::set(key, &data, 10).await?;
|
||||
|
@ -106,7 +106,7 @@ pub async fn set<V: for<'a> Deserialize<'a> + Serialize>(
|
|||
/// # use backend_rs::database::cache;
|
||||
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key = "banana";
|
||||
/// let data = "I want to cache this string".to_string();
|
||||
/// let data = "I want to cache this string".to_owned();
|
||||
///
|
||||
/// // set cache
|
||||
/// cache::set(key, &data, 10).await?;
|
||||
|
@ -145,7 +145,7 @@ pub async fn get<V: for<'a> Deserialize<'a> + Serialize>(key: &str) -> Result<Op
|
|||
/// # use backend_rs::database::cache;
|
||||
/// # async fn f() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let key = "chocolate";
|
||||
/// let value = "I want to cache this string".to_string();
|
||||
/// let value = "I want to cache this string".to_owned();
|
||||
///
|
||||
/// // set cache
|
||||
/// cache::set(key, &value, 10).await?;
|
||||
|
@ -248,12 +248,12 @@ mod unit_test {
|
|||
let value_1: Vec<i32> = vec![1, 2, 3, 4, 5];
|
||||
|
||||
let key_2 = "CARGO_TEST_CACHE_KEY_2";
|
||||
let value_2 = "Hello fedizens".to_string();
|
||||
let value_2 = "Hello fedizens".to_owned();
|
||||
|
||||
let key_3 = "CARGO_TEST_CACHE_KEY_3";
|
||||
let value_3 = Data {
|
||||
id: 1000000007,
|
||||
kind: "prime number".to_string(),
|
||||
kind: "prime number".to_owned(),
|
||||
};
|
||||
|
||||
set(key_1, &value_1, 1).await.unwrap();
|
||||
|
@ -287,7 +287,7 @@ mod unit_test {
|
|||
let key_2 = "fish";
|
||||
let key_3 = "awawa";
|
||||
|
||||
let value_1 = "hello".to_string();
|
||||
let value_1 = "hello".to_owned();
|
||||
let value_2 = 998244353u32;
|
||||
let value_3 = 'あ';
|
||||
|
||||
|
|
|
@ -57,12 +57,12 @@ async fn init_conn_pool() -> Result<(), RedisError> {
|
|||
};
|
||||
|
||||
if let Some(user) = &redis.user {
|
||||
params.push(user.to_string())
|
||||
params.push(user.to_owned())
|
||||
}
|
||||
if let Some(pass) = &redis.pass {
|
||||
params.push(format!(":{}@", pass))
|
||||
params.push(format!(":{}@", urlencoding::encode(pass)))
|
||||
}
|
||||
params.push(redis.host.to_string());
|
||||
params.push(redis.host.to_owned());
|
||||
params.push(format!(":{}", redis.port));
|
||||
params.push(format!("/{}", redis.db));
|
||||
|
||||
|
@ -111,8 +111,8 @@ pub async fn get_conn() -> Result<PooledConnection<'static, RedisConnectionManag
|
|||
|
||||
/// prefix Redis key
|
||||
#[inline]
|
||||
pub fn key(key: impl ToString) -> String {
|
||||
format!("{}:{}", CONFIG.redis_key_prefix, key.to_string())
|
||||
pub fn key(key: impl std::fmt::Display) -> String {
|
||||
format!("{}:{}", CONFIG.redis_key_prefix, key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -8,7 +8,7 @@ pub struct Acct {
|
|||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[doc = "Error type to indicate a string-to-[`Acct`] conversion failure"]
|
||||
#[doc = "Error type to indicate a [`String`]-to-[`Acct`] conversion failure"]
|
||||
#[error("failed to convert string '{0}' into acct")]
|
||||
pub struct InvalidAcctString(String);
|
||||
|
||||
|
@ -25,11 +25,11 @@ impl FromStr for Acct {
|
|||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
username: split[0].to_string(),
|
||||
username: split[0].to_owned(),
|
||||
host: if split.len() == 1 {
|
||||
None
|
||||
} else {
|
||||
Some(split[1].to_string())
|
||||
Some(split[1].to_owned())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -70,11 +70,11 @@ mod unit_test {
|
|||
#[test]
|
||||
fn acct_to_string() {
|
||||
let remote_acct = Acct {
|
||||
username: "firefish".to_string(),
|
||||
host: Some("example.com".to_string()),
|
||||
username: "firefish".to_owned(),
|
||||
host: Some("example.com".to_owned()),
|
||||
};
|
||||
let local_acct = Acct {
|
||||
username: "MisakaMikoto".to_string(),
|
||||
username: "MisakaMikoto".to_owned(),
|
||||
host: None,
|
||||
};
|
||||
|
||||
|
@ -87,11 +87,11 @@ mod unit_test {
|
|||
#[test]
|
||||
fn string_to_acct() {
|
||||
let remote_acct = Acct {
|
||||
username: "firefish".to_string(),
|
||||
host: Some("example.com".to_string()),
|
||||
username: "firefish".to_owned(),
|
||||
host: Some("example.com".to_owned()),
|
||||
};
|
||||
let local_acct = Acct {
|
||||
username: "MisakaMikoto".to_string(),
|
||||
username: "MisakaMikoto".to_owned(),
|
||||
host: None,
|
||||
};
|
||||
|
||||
|
|
87
packages/backend-rs/src/federation/internal_actor/cache.rs
Normal file
87
packages/backend-rs/src/federation/internal_actor/cache.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
//! In-memory internal actor cache handler
|
||||
|
||||
// TODO: refactoring
|
||||
|
||||
use super::*;
|
||||
use crate::{database::db_conn, model::entity::user};
|
||||
use sea_orm::prelude::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
#[doc = "database error"]
|
||||
Db(#[from] DbErr),
|
||||
#[error("{} does not exist", Acct::from(.0.to_owned()))]
|
||||
#[doc = "internal actor does not exist"]
|
||||
InternalActorNotFound(InternalActor),
|
||||
}
|
||||
|
||||
static INSTANCE_ACTOR: Mutex<Option<user::Model>> = Mutex::new(None);
|
||||
static RELAY_ACTOR: Mutex<Option<user::Model>> = Mutex::new(None);
|
||||
|
||||
fn set_instance_actor(value: &user::Model) {
|
||||
let _ = INSTANCE_ACTOR
|
||||
.lock()
|
||||
.map(|mut cache| *cache = Some(value.to_owned()));
|
||||
}
|
||||
fn set_relay_actor(value: &user::Model) {
|
||||
let _ = RELAY_ACTOR
|
||||
.lock()
|
||||
.map(|mut cache| *cache = Some(value.to_owned()));
|
||||
}
|
||||
|
||||
async fn cache_instance_actor() -> Result<user::Model, Error> {
|
||||
let actor = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(INSTANCE_ACTOR_USERNAME))
|
||||
.filter(user::Column::Host.is_null())
|
||||
.one(db_conn().await?)
|
||||
.await?;
|
||||
|
||||
if let Some(actor) = actor {
|
||||
set_instance_actor(&actor);
|
||||
Ok(actor)
|
||||
} else {
|
||||
Err(Error::InternalActorNotFound(InternalActor::Instance))
|
||||
}
|
||||
}
|
||||
async fn cache_relay_actor() -> Result<user::Model, Error> {
|
||||
let actor = user::Entity::find()
|
||||
.filter(user::Column::Username.eq(RELAY_ACTOR_USERNAME))
|
||||
.filter(user::Column::Host.is_null())
|
||||
.one(db_conn().await?)
|
||||
.await?;
|
||||
|
||||
if let Some(actor) = actor {
|
||||
set_relay_actor(&actor);
|
||||
Ok(actor)
|
||||
} else {
|
||||
Err(Error::InternalActorNotFound(InternalActor::Relay))
|
||||
}
|
||||
}
|
||||
|
||||
// for napi export
|
||||
// https://github.com/napi-rs/napi-rs/issues/2060
|
||||
type User = user::Model;
|
||||
|
||||
#[macros::export(js_name = "getInternalActor")]
|
||||
pub async fn get(actor: InternalActor) -> Result<User, Error> {
|
||||
match actor {
|
||||
InternalActor::Instance => {
|
||||
if let Some(cache) = INSTANCE_ACTOR.lock().ok().and_then(|cache| cache.clone()) {
|
||||
tracing::debug!("Using cached instance.actor");
|
||||
return Ok(cache);
|
||||
}
|
||||
tracing::debug!("Caching instance.actor");
|
||||
cache_instance_actor().await
|
||||
}
|
||||
InternalActor::Relay => {
|
||||
if let Some(cache) = RELAY_ACTOR.lock().ok().and_then(|cache| cache.clone()) {
|
||||
tracing::debug!("Using cached relay.actor");
|
||||
return Ok(cache);
|
||||
}
|
||||
tracing::debug!("Caching relay.actor");
|
||||
cache_relay_actor().await
|
||||
}
|
||||
}
|
||||
}
|
34
packages/backend-rs/src/federation/internal_actor/mod.rs
Normal file
34
packages/backend-rs/src/federation/internal_actor/mod.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
mod cache;
|
||||
|
||||
pub use cache::get;
|
||||
|
||||
use super::acct::Acct;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[macros::derive_clone_and_export(string_enum = "lowercase")]
|
||||
pub enum InternalActor {
|
||||
Instance,
|
||||
Relay,
|
||||
}
|
||||
|
||||
const INSTANCE_ACTOR_USERNAME: &str = "instance.actor";
|
||||
const RELAY_ACTOR_USERNAME: &str = "relay.actor";
|
||||
|
||||
// TODO: When `std::mem::variant_count` is stabilized, use
|
||||
// it to count system actors instead of hard coding the magic number
|
||||
pub const INTERNAL_ACTORS: u64 = 2;
|
||||
|
||||
impl From<InternalActor> for Acct {
|
||||
fn from(actor: InternalActor) -> Self {
|
||||
match actor {
|
||||
InternalActor::Instance => Acct {
|
||||
username: INSTANCE_ACTOR_USERNAME.to_owned(),
|
||||
host: None,
|
||||
},
|
||||
InternalActor::Relay => Acct {
|
||||
username: RELAY_ACTOR_USERNAME.to_owned(),
|
||||
host: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
//! Services used to federate with other servers
|
||||
|
||||
pub mod acct;
|
||||
pub mod internal_actor;
|
||||
pub mod nodeinfo;
|
||||
|
|
|
@ -109,12 +109,12 @@ mod unit_test {
|
|||
let links_1 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy".to_string(),
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_owned(),
|
||||
href: "https://example.com/dummy".to_owned(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_string(),
|
||||
href: "https://example.com/real".to_string(),
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0".to_owned(),
|
||||
href: "https://example.com/real".to_owned(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -126,12 +126,12 @@ mod unit_test {
|
|||
let links_2 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy".to_string(),
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_owned(),
|
||||
href: "https://example.com/dummy".to_owned(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".to_string(),
|
||||
href: "https://example.com/real".to_string(),
|
||||
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1".to_owned(),
|
||||
href: "https://example.com/real".to_owned(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -143,12 +143,12 @@ mod unit_test {
|
|||
let links_3 = NodeinfoLinks {
|
||||
links: vec![
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_string(),
|
||||
href: "https://example.com/dummy/2.0".to_string(),
|
||||
rel: "https://example.com/incorrect/schema/2.0".to_owned(),
|
||||
href: "https://example.com/dummy/2.0".to_owned(),
|
||||
},
|
||||
NodeinfoLink {
|
||||
rel: "https://example.com/incorrect/schema/2.1".to_string(),
|
||||
href: "https://example.com/dummy/2.1".to_string(),
|
||||
rel: "https://example.com/incorrect/schema/2.1".to_owned(),
|
||||
href: "https://example.com/dummy/2.1".to_owned(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::{
|
|||
config::{local_server_info, CONFIG},
|
||||
database::db_conn,
|
||||
federation::nodeinfo::schema::*,
|
||||
misc,
|
||||
model::entity::{note, user},
|
||||
};
|
||||
use sea_orm::prelude::*;
|
||||
|
@ -33,9 +34,10 @@ async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> {
|
|||
const MONTH: chrono::TimeDelta = chrono::Duration::days(30);
|
||||
const HALF_YEAR: chrono::TimeDelta = chrono::Duration::days(183);
|
||||
|
||||
let local_users = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.count(db);
|
||||
let local_users = misc::user::count::local_total(db);
|
||||
|
||||
// We don't need to care about the number of system actors here,
|
||||
// because their last active date is null
|
||||
let local_active_halfyear = user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.filter(user::Column::LastActiveDate.gt(now - HALF_YEAR))
|
||||
|
@ -66,45 +68,45 @@ async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, DbErr> {
|
|||
let meta = local_server_info().await?;
|
||||
let mut metadata = HashMap::from([
|
||||
(
|
||||
"nodeName".to_string(),
|
||||
"nodeName".to_owned(),
|
||||
json!(meta.name.unwrap_or_else(|| CONFIG.host.clone())),
|
||||
),
|
||||
("nodeDescription".to_string(), json!(meta.description)),
|
||||
("repositoryUrl".to_string(), json!(meta.repository_url)),
|
||||
("nodeDescription".to_owned(), json!(meta.description)),
|
||||
("repositoryUrl".to_owned(), json!(meta.repository_url)),
|
||||
(
|
||||
"enableLocalTimeline".to_string(),
|
||||
"enableLocalTimeline".to_owned(),
|
||||
json!(!meta.disable_local_timeline),
|
||||
),
|
||||
(
|
||||
"enableRecommendedTimeline".to_string(),
|
||||
"enableRecommendedTimeline".to_owned(),
|
||||
json!(!meta.disable_recommended_timeline),
|
||||
),
|
||||
(
|
||||
"enableGlobalTimeline".to_string(),
|
||||
"enableGlobalTimeline".to_owned(),
|
||||
json!(!meta.disable_global_timeline),
|
||||
),
|
||||
(
|
||||
"enableGuestTimeline".to_string(),
|
||||
"enableGuestTimeline".to_owned(),
|
||||
json!(meta.enable_guest_timeline),
|
||||
),
|
||||
(
|
||||
"maintainer".to_string(),
|
||||
"maintainer".to_owned(),
|
||||
json!({"name":meta.maintainer_name,"email":meta.maintainer_email}),
|
||||
),
|
||||
("proxyAccountName".to_string(), json!(meta.proxy_account_id)),
|
||||
("proxyAccountName".to_owned(), json!(meta.proxy_account_id)),
|
||||
(
|
||||
"themeColor".to_string(),
|
||||
json!(meta.theme_color.unwrap_or_else(|| "#31748f".to_string())),
|
||||
"themeColor".to_owned(),
|
||||
json!(meta.theme_color.unwrap_or_else(|| "#31748f".to_owned())),
|
||||
),
|
||||
]);
|
||||
metadata.shrink_to_fit();
|
||||
|
||||
Ok(Nodeinfo21 {
|
||||
software: Software21 {
|
||||
name: "firefish".to_string(),
|
||||
name: "firefish".to_owned(),
|
||||
version: CONFIG.version.clone(),
|
||||
repository: Some(meta.repository_url),
|
||||
homepage: Some("https://firefish.dev/firefish/firefish".to_string()),
|
||||
homepage: Some("https://firefish.dev/firefish/firefish".to_owned()),
|
||||
},
|
||||
protocols: vec![Protocol::Activitypub],
|
||||
services: Services {
|
||||
|
|
|
@ -18,15 +18,15 @@ pub fn initialize_logger() {
|
|||
});
|
||||
} else if let Some(levels) = &CONFIG.log_level {
|
||||
// `logLevel` config is Deprecated
|
||||
if levels.contains(&"trace".to_string()) {
|
||||
if levels.contains(&"trace".to_owned()) {
|
||||
builder = builder.with_max_level(Level::TRACE);
|
||||
} else if levels.contains(&"debug".to_string()) {
|
||||
} else if levels.contains(&"debug".to_owned()) {
|
||||
builder = builder.with_max_level(Level::DEBUG);
|
||||
} else if levels.contains(&"info".to_string()) {
|
||||
} else if levels.contains(&"info".to_owned()) {
|
||||
builder = builder.with_max_level(Level::INFO);
|
||||
} else if levels.contains(&"warning".to_string()) {
|
||||
} else if levels.contains(&"warning".to_owned()) {
|
||||
builder = builder.with_max_level(Level::WARN);
|
||||
} else if levels.contains(&"error".to_string()) {
|
||||
} else if levels.contains(&"error".to_owned()) {
|
||||
builder = builder.with_max_level(Level::ERROR);
|
||||
} else {
|
||||
// Fallback
|
||||
|
|
|
@ -26,19 +26,19 @@ pub fn show_server_info() -> Result<(), SysinfoPoisonError> {
|
|||
|
||||
tracing::info!(
|
||||
"Hostname: {}",
|
||||
System::host_name().unwrap_or_else(|| "unknown".to_string())
|
||||
System::host_name().unwrap_or_else(|| "unknown".to_owned())
|
||||
);
|
||||
tracing::info!(
|
||||
"OS: {}",
|
||||
System::long_os_version().unwrap_or_else(|| "unknown".to_string())
|
||||
System::long_os_version().unwrap_or_else(|| "unknown".to_owned())
|
||||
);
|
||||
tracing::info!(
|
||||
"Kernel: {}",
|
||||
System::kernel_version().unwrap_or_else(|| "unknown".to_string())
|
||||
System::kernel_version().unwrap_or_else(|| "unknown".to_owned())
|
||||
);
|
||||
tracing::info!(
|
||||
"CPU architecture: {}",
|
||||
System::cpu_arch().unwrap_or_else(|| "unknown".to_string())
|
||||
System::cpu_arch().unwrap_or_else(|| "unknown".to_owned())
|
||||
);
|
||||
tracing::info!("CPU threads: {}", system_info.cpus().len());
|
||||
tracing::info!("Total memory: {} MiB", system_info.total_memory() / 1048576);
|
||||
|
|
|
@ -81,7 +81,7 @@ pub async fn is_allowed_server(host: &str) -> Result<bool, sea_orm::DbErr> {
|
|||
return Ok(true);
|
||||
}
|
||||
if let Some(allowed_hosts) = meta.allowed_hosts {
|
||||
return Ok(allowed_hosts.contains(&host.to_string()));
|
||||
return Ok(allowed_hosts.contains(&host.to_owned()));
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
|
|
@ -73,114 +73,114 @@ mod unit_test {
|
|||
#[test]
|
||||
fn word_mute_match() {
|
||||
let texts = [
|
||||
"The quick brown fox jumps over the lazy dog.".to_string(),
|
||||
"色は匂へど 散りぬるを 我が世誰ぞ 常ならむ".to_string(),
|
||||
"😇".to_string(),
|
||||
"The quick brown fox jumps over the lazy dog.".to_owned(),
|
||||
"色は匂へど 散りぬるを 我が世誰ぞ 常ならむ".to_owned(),
|
||||
"😇".to_owned(),
|
||||
];
|
||||
|
||||
let hiragana_1 = r"/[\u{3040}-\u{309f}]/u".to_string();
|
||||
let hiragana_2 = r"/[あ-ん]/u".to_string();
|
||||
let katakana_1 = r"/[\u{30a1}-\u{30ff}]/u".to_string();
|
||||
let katakana_2 = r"/[ア-ン]/u".to_string();
|
||||
let emoji = r"/[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/u".to_string();
|
||||
let hiragana_1 = r"/[\u{3040}-\u{309f}]/u".to_owned();
|
||||
let hiragana_2 = r"/[あ-ん]/u".to_owned();
|
||||
let katakana_1 = r"/[\u{30a1}-\u{30ff}]/u".to_owned();
|
||||
let katakana_2 = r"/[ア-ン]/u".to_owned();
|
||||
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_owned();
|
||||
|
||||
assert!(check_word_mute_impl(&texts, &[], &["/the/i".to_string()]));
|
||||
assert!(check_word_mute_impl(&texts, &[], &["/the/i".to_owned()]));
|
||||
|
||||
assert!(!check_word_mute_impl(&texts, &[], &["/the/".to_string()]));
|
||||
assert!(!check_word_mute_impl(&texts, &[], &["/the/".to_owned()]));
|
||||
|
||||
assert!(check_word_mute_impl(&texts, &[], &["/QuICk/i".to_string()]));
|
||||
assert!(check_word_mute_impl(&texts, &[], &["/QuICk/i".to_owned()]));
|
||||
|
||||
assert!(!check_word_mute_impl(&texts, &[], &["/QuICk/".to_string()]));
|
||||
assert!(!check_word_mute_impl(&texts, &[], &["/QuICk/".to_owned()]));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&[
|
||||
"我".to_string(),
|
||||
"有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()
|
||||
"我".to_owned(),
|
||||
"有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_owned()
|
||||
],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_string()],
|
||||
&["有為の奥山 今日越えて 浅き夢見し 酔ひもせず".to_owned()],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&[
|
||||
"有為の奥山".to_string(),
|
||||
"今日越えて".to_string(),
|
||||
"浅き夢見し".to_string(),
|
||||
"酔ひもせず".to_string()
|
||||
"有為の奥山".to_owned(),
|
||||
"今日越えて".to_owned(),
|
||||
"浅き夢見し".to_owned(),
|
||||
"酔ひもせず".to_owned()
|
||||
],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&["yellow fox".to_owned(), "mastodon".to_owned()],
|
||||
&[hiragana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&["yellow fox".to_owned(), "mastodon".to_owned()],
|
||||
&[hiragana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&["yellow fox".to_owned(), "mastodon".to_owned()],
|
||||
&[katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&["yellow fox".to_owned(), "mastodon".to_owned()],
|
||||
&[katakana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["brown fox".to_string(), "mastodon".to_string()],
|
||||
&["brown fox".to_owned(), "mastodon".to_owned()],
|
||||
&[katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["brown fox".to_string(), "mastodon".to_string()],
|
||||
&["brown fox".to_owned(), "mastodon".to_owned()],
|
||||
&[katakana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "dog".to_string()],
|
||||
&["yellow fox".to_owned(), "dog".to_owned()],
|
||||
&[katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "dog".to_string()],
|
||||
&["yellow fox".to_owned(), "dog".to_owned()],
|
||||
&[katakana_2.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["yellow fox".to_string(), "mastodon".to_string()],
|
||||
&["yellow fox".to_owned(), "mastodon".to_owned()],
|
||||
&[hiragana_1.clone(), katakana_1.clone()]
|
||||
));
|
||||
|
||||
assert!(check_word_mute_impl(
|
||||
&texts,
|
||||
&["😇".to_string(), "🥲".to_string(), "🥴".to_string()],
|
||||
&["😇".to_owned(), "🥲".to_owned(), "🥴".to_owned()],
|
||||
&[]
|
||||
));
|
||||
|
||||
assert!(!check_word_mute_impl(
|
||||
&texts,
|
||||
&["🙂".to_string(), "🥲".to_string(), "🥴".to_string()],
|
||||
&["🙂".to_owned(), "🥲".to_owned(), "🥴".to_owned()],
|
||||
&[]
|
||||
));
|
||||
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
/// Escapes `%` and `\` in the given string.
|
||||
#[macros::export]
|
||||
pub fn sql_like_escape(src: &str) -> String {
|
||||
src.replace('%', r"\%").replace('_', r"\_")
|
||||
}
|
||||
|
||||
#[macros::export]
|
||||
pub fn sql_regex_escape(src: &str) -> String {
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[!$()*+.:<=>?\[\]\^{|}-]").unwrap());
|
||||
RE.replace_all(src, r"\$1").to_string()
|
||||
}
|
||||
|
||||
/// Returns `true` if `src` does not contain suspicious characters like `%`.
|
||||
#[macros::export]
|
||||
pub fn safe_for_sql(src: &str) -> bool {
|
||||
|
|
|
@ -68,7 +68,7 @@ pub async fn get_image_size_from_url(url: &str) -> Result<ImageSize, Error> {
|
|||
|
||||
if attempted {
|
||||
tracing::warn!("attempt limit exceeded: {}", url);
|
||||
return Err(Error::TooManyAttempts(url.to_string()));
|
||||
return Err(Error::TooManyAttempts(url.to_owned()));
|
||||
}
|
||||
|
||||
tracing::info!("retrieving image from {}", url);
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
#[macros::export]
|
||||
pub fn to_mastodon_id(firefish_id: &str) -> Option<String> {
|
||||
let decoded: [u8; 16] = basen::BASE36.decode_var_len(firefish_id)?;
|
||||
Some(basen::BASE10.encode_var_len(&decoded))
|
||||
}
|
||||
|
||||
#[macros::export]
|
||||
pub fn from_mastodon_id(mastodon_id: &str) -> Option<String> {
|
||||
let decoded: [u8; 16] = basen::BASE10.decode_var_len(mastodon_id)?;
|
||||
Some(basen::BASE36.encode_var_len(&decoded))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn to_mastodon_id() {
|
||||
assert_eq!(
|
||||
super::to_mastodon_id("9pdqi3rjl4lxirq3").unwrap(),
|
||||
"2145531976185871567229403"
|
||||
);
|
||||
assert_eq!(super::to_mastodon_id("9pdqi3r*irq3"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_mastodon_id() {
|
||||
assert_eq!(
|
||||
super::from_mastodon_id("2145531976185871567229403").unwrap(),
|
||||
"9pdqi3rjl4lxirq3"
|
||||
);
|
||||
assert_eq!(super::from_mastodon_id("9pdqi3rjl4lxirq3"), None);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ pub mod get_image_size;
|
|||
pub mod is_quote;
|
||||
pub mod is_safe_url;
|
||||
pub mod latest_version;
|
||||
pub mod mastodon_id;
|
||||
pub mod note;
|
||||
pub mod nyaify;
|
||||
pub mod password;
|
||||
|
@ -18,3 +17,4 @@ pub mod reaction;
|
|||
pub mod remove_old_attestation_challenges;
|
||||
pub mod should_nyaify;
|
||||
pub mod system_info;
|
||||
pub mod user;
|
||||
|
|
|
@ -15,12 +15,12 @@ pub fn summarize_impl(
|
|||
|
||||
match file_ids.len() {
|
||||
0 => (),
|
||||
1 => buf.push("📎".to_string()),
|
||||
1 => buf.push("📎".to_owned()),
|
||||
n => buf.push(format!("📎 ({})", n)),
|
||||
};
|
||||
|
||||
if has_poll {
|
||||
buf.push("📊".to_string())
|
||||
buf.push("📊".to_owned())
|
||||
}
|
||||
|
||||
buf.join(" ")
|
||||
|
@ -94,7 +94,7 @@ mod unit_test {
|
|||
fn summarize_note() {
|
||||
let note = NoteLike {
|
||||
file_ids: vec![],
|
||||
text: Some("Hello world!".to_string()),
|
||||
text: Some("Hello world!".to_owned()),
|
||||
cw: None,
|
||||
has_poll: false,
|
||||
};
|
||||
|
@ -102,26 +102,26 @@ mod unit_test {
|
|||
|
||||
let note_with_cw = NoteLike {
|
||||
file_ids: vec![],
|
||||
text: Some("Hello world!".to_string()),
|
||||
cw: Some("Content warning".to_string()),
|
||||
text: Some("Hello world!".to_owned()),
|
||||
cw: Some("Content warning".to_owned()),
|
||||
has_poll: false,
|
||||
};
|
||||
assert_eq!(summarize!(note_with_cw), "Content warning");
|
||||
|
||||
let note_with_file_and_cw = NoteLike {
|
||||
file_ids: vec!["9s7fmcqogiq4igin".to_string()],
|
||||
file_ids: vec!["9s7fmcqogiq4igin".to_owned()],
|
||||
text: None,
|
||||
cw: Some("Selfie, no ec".to_string()),
|
||||
cw: Some("Selfie, no ec".to_owned()),
|
||||
has_poll: false,
|
||||
};
|
||||
assert_eq!(summarize!(note_with_file_and_cw), "Selfie, no ec 📎");
|
||||
|
||||
let note_with_files_only = NoteLike {
|
||||
file_ids: vec![
|
||||
"9s7fmcqogiq4igin".to_string(),
|
||||
"9s7qrld5u14cey98".to_string(),
|
||||
"9s7gebs5zgts4kca".to_string(),
|
||||
"9s5z3e4vefqd29ee".to_string(),
|
||||
"9s7fmcqogiq4igin".to_owned(),
|
||||
"9s7qrld5u14cey98".to_owned(),
|
||||
"9s7gebs5zgts4kca".to_owned(),
|
||||
"9s5z3e4vefqd29ee".to_owned(),
|
||||
],
|
||||
text: None,
|
||||
cw: None,
|
||||
|
@ -131,13 +131,13 @@ mod unit_test {
|
|||
|
||||
let note_all = NoteLike {
|
||||
file_ids: vec![
|
||||
"9s7fmcqogiq4igin".to_string(),
|
||||
"9s7qrld5u14cey98".to_string(),
|
||||
"9s7gebs5zgts4kca".to_string(),
|
||||
"9s5z3e4vefqd29ee".to_string(),
|
||||
"9s7fmcqogiq4igin".to_owned(),
|
||||
"9s7qrld5u14cey98".to_owned(),
|
||||
"9s7gebs5zgts4kca".to_owned(),
|
||||
"9s5z3e4vefqd29ee".to_owned(),
|
||||
],
|
||||
text: Some("Hello world!".to_string()),
|
||||
cw: Some("Content warning".to_string()),
|
||||
text: Some("Hello world!".to_owned()),
|
||||
cw: Some("Content warning".to_owned()),
|
||||
has_poll: true,
|
||||
};
|
||||
assert_eq!(summarize!(note_all), "Content warning 📎 (4) 📊");
|
||||
|
|
|
@ -128,12 +128,12 @@ mod unit_test {
|
|||
#[test]
|
||||
fn decode_reaction() {
|
||||
let unicode_emoji_1 = DecodedReaction {
|
||||
reaction: "⭐".to_string(),
|
||||
reaction: "⭐".to_owned(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
let unicode_emoji_2 = DecodedReaction {
|
||||
reaction: "🩷".to_string(),
|
||||
reaction: "🩷".to_owned(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
|
@ -145,23 +145,23 @@ mod unit_test {
|
|||
assert_ne!(super::decode_reaction("🩷"), unicode_emoji_1);
|
||||
|
||||
let unicode_emoji_3 = DecodedReaction {
|
||||
reaction: "🖖🏿".to_string(),
|
||||
reaction: "🖖🏿".to_owned(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(super::decode_reaction("🖖🏿"), unicode_emoji_3);
|
||||
|
||||
let local_emoji = DecodedReaction {
|
||||
reaction: ":meow_melt_tears@.:".to_string(),
|
||||
name: Some("meow_melt_tears".to_string()),
|
||||
reaction: ":meow_melt_tears@.:".to_owned(),
|
||||
name: Some("meow_melt_tears".to_owned()),
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(super::decode_reaction(":meow_melt_tears:"), local_emoji);
|
||||
|
||||
let remote_emoji_1 = DecodedReaction {
|
||||
reaction: ":meow_uwu@some-domain.example.org:".to_string(),
|
||||
name: Some("meow_uwu".to_string()),
|
||||
host: Some("some-domain.example.org".to_string()),
|
||||
reaction: ":meow_uwu@some-domain.example.org:".to_owned(),
|
||||
name: Some("meow_uwu".to_owned()),
|
||||
host: Some("some-domain.example.org".to_owned()),
|
||||
};
|
||||
assert_eq!(
|
||||
super::decode_reaction(":meow_uwu@some-domain.example.org:"),
|
||||
|
@ -169,9 +169,9 @@ mod unit_test {
|
|||
);
|
||||
|
||||
let remote_emoji_2 = DecodedReaction {
|
||||
reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(),
|
||||
name: Some("C++23".to_string()),
|
||||
host: Some("xn--eckwd4c7c.example.org".to_string()),
|
||||
reaction: ":C++23@xn--eckwd4c7c.example.org:".to_owned(),
|
||||
name: Some("C++23".to_owned()),
|
||||
host: Some("xn--eckwd4c7c.example.org".to_owned()),
|
||||
};
|
||||
assert_eq!(
|
||||
super::decode_reaction(":C++23@xn--eckwd4c7c.example.org:"),
|
||||
|
@ -179,14 +179,14 @@ mod unit_test {
|
|||
);
|
||||
|
||||
let invalid_reaction_1 = DecodedReaction {
|
||||
reaction: ":foo".to_string(),
|
||||
reaction: ":foo".to_owned(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
assert_eq!(super::decode_reaction(":foo"), invalid_reaction_1);
|
||||
|
||||
let invalid_reaction_2 = DecodedReaction {
|
||||
reaction: ":foo&@example.com:".to_string(),
|
||||
reaction: ":foo&@example.com:".to_owned(),
|
||||
name: None,
|
||||
host: None,
|
||||
};
|
||||
|
|
|
@ -38,9 +38,9 @@ pub fn cpu_info() -> Result<Cpu, SysinfoPoisonError> {
|
|||
model: match system_info.cpus() {
|
||||
[] => {
|
||||
tracing::debug!("failed to get CPU info");
|
||||
"unknown".to_string()
|
||||
"unknown".to_owned()
|
||||
}
|
||||
cpus => cpus[0].brand().to_string(),
|
||||
cpus => cpus[0].brand().to_owned(),
|
||||
},
|
||||
cores: system_info.cpus().len() as u16,
|
||||
})
|
||||
|
|
17
packages/backend-rs/src/misc/user/count.rs
Normal file
17
packages/backend-rs/src/misc/user/count.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use crate::{federation::internal_actor::INTERNAL_ACTORS, model::entity::user};
|
||||
use sea_orm::prelude::*;
|
||||
|
||||
pub async fn local_total(db: &DbConn) -> Result<u64, DbErr> {
|
||||
user::Entity::find()
|
||||
.filter(user::Column::Host.is_null())
|
||||
.count(db)
|
||||
.await
|
||||
.map(|count| count - INTERNAL_ACTORS)
|
||||
}
|
||||
|
||||
#[macros::ts_export(js_name = "countLocalUsers")]
|
||||
pub async fn local_total_js() -> Result<u32, DbErr> {
|
||||
local_total(crate::database::db_conn().await?)
|
||||
.await
|
||||
.map(|count| count as u32)
|
||||
}
|
1
packages/backend-rs/src/misc/user/mod.rs
Normal file
1
packages/backend-rs/src/misc/user/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod count;
|
|
@ -15,7 +15,7 @@ pub struct Model {
|
|||
pub token: String,
|
||||
pub hash: String,
|
||||
#[sea_orm(column_name = "userId")]
|
||||
pub user_id: String,
|
||||
pub user_id: Option<String>,
|
||||
#[sea_orm(column_name = "appId")]
|
||||
pub app_id: Option<String>,
|
||||
#[sea_orm(column_name = "lastUsedAt")]
|
||||
|
@ -41,6 +41,8 @@ pub enum Relation {
|
|||
App,
|
||||
#[sea_orm(has_many = "super::notification::Entity")]
|
||||
Notification,
|
||||
#[sea_orm(has_many = "super::sw_subscription::Entity")]
|
||||
SwSubscription,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
|
@ -63,6 +65,12 @@ impl Related<super::notification::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::sw_subscription::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SwSubscription.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
|
|
|
@ -128,6 +128,36 @@ pub enum PollNoteVisibility {
|
|||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[macros::derive_clone_and_export(string_enum = "camelCase")]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
enum_name = "push_subscription_type"
|
||||
)]
|
||||
pub enum PushSubscriptionType {
|
||||
#[sea_orm(string_value = "admin.report")]
|
||||
AdminReport,
|
||||
#[sea_orm(string_value = "admin.sign_up")]
|
||||
AdminSignUp,
|
||||
#[sea_orm(string_value = "favourite")]
|
||||
Favourite,
|
||||
#[sea_orm(string_value = "follow")]
|
||||
Follow,
|
||||
#[sea_orm(string_value = "follow_request")]
|
||||
FollowRequest,
|
||||
#[sea_orm(string_value = "mention")]
|
||||
Mention,
|
||||
#[sea_orm(string_value = "poll")]
|
||||
Poll,
|
||||
#[sea_orm(string_value = "reblog")]
|
||||
Reblog,
|
||||
#[sea_orm(string_value = "status")]
|
||||
Status,
|
||||
#[sea_orm(string_value = "update")]
|
||||
Update,
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[macros::derive_clone_and_export(string_enum = "camelCase")]
|
||||
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "relay_status")]
|
||||
pub enum RelayStatus {
|
||||
#[sea_orm(string_value = "accepted")]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
|
||||
|
||||
use super::sea_orm_active_enums::PushSubscriptionType;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -19,10 +20,22 @@ pub struct Model {
|
|||
pub publickey: String,
|
||||
#[sea_orm(column_name = "sendReadMessage")]
|
||||
pub send_read_message: bool,
|
||||
#[sea_orm(column_name = "appAccessTokenId")]
|
||||
pub app_access_token_id: Option<String>,
|
||||
#[sea_orm(column_name = "subscriptionTypes")]
|
||||
pub subscription_types: Vec<PushSubscriptionType>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::access_token::Entity",
|
||||
from = "Column::AppAccessTokenId",
|
||||
to = "super::access_token::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
AccessToken,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
|
@ -33,6 +46,12 @@ pub enum Relation {
|
|||
User,
|
||||
}
|
||||
|
||||
impl Related<super::access_token::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AccessToken.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
|
|
|
@ -68,6 +68,8 @@ pub struct Model {
|
|||
pub is_indexable: bool,
|
||||
#[sea_orm(column_name = "mutedPatterns")]
|
||||
pub muted_patterns: Vec<String>,
|
||||
#[sea_orm(column_type = "JsonBinary")]
|
||||
pub mentions: Json,
|
||||
#[sea_orm(column_name = "mutedInstances")]
|
||||
pub muted_instances: Vec<String>,
|
||||
#[sea_orm(column_name = "mutedWords")]
|
||||
|
|
|
@ -71,7 +71,7 @@ async fn add_note_to_antenna(antenna_id: &str, note: &Note) -> Result<(), Error>
|
|||
.await?;
|
||||
|
||||
// for streaming API
|
||||
stream::antenna::publish(antenna_id.to_string(), note).await?;
|
||||
stream::antenna::publish(antenna_id.to_owned(), note).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ pub async fn watch_note(
|
|||
note_watching::Entity::insert(note_watching::ActiveModel {
|
||||
id: ActiveValue::set(gen_id_at(now)),
|
||||
created_at: ActiveValue::set(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()),
|
||||
user_id: ActiveValue::Set(watcher_id.to_owned()),
|
||||
note_user_id: ActiveValue::Set(note_author_id.to_owned()),
|
||||
note_id: ActiveValue::Set(note_id.to_owned()),
|
||||
})
|
||||
.exec(db_conn().await?)
|
||||
.await?;
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
use crate::{
|
||||
config::local_server_info, database::db_conn, misc::note::summarize,
|
||||
model::entity::sw_subscription, util::http_client,
|
||||
config::local_server_info,
|
||||
database::db_conn,
|
||||
misc::note::summarize,
|
||||
model::entity::{access_token, app, sw_subscription},
|
||||
util::{
|
||||
http_client,
|
||||
id::{get_timestamp, InvalidIdError},
|
||||
},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use sea_orm::prelude::*;
|
||||
|
@ -19,6 +25,11 @@ pub enum Error {
|
|||
#[doc = "provided content is invalid"]
|
||||
#[error("invalid content ({0})")]
|
||||
InvalidContent(String),
|
||||
#[doc = "found Mastodon subscription is invalid"]
|
||||
#[error("invalid subscription ({0})")]
|
||||
InvalidSubscription(String),
|
||||
#[error("invalid notification ID")]
|
||||
InvalidId(#[from] InvalidIdError),
|
||||
#[error("failed to acquire an HTTP client")]
|
||||
HttpClient(#[from] http_client::Error),
|
||||
}
|
||||
|
@ -44,7 +55,7 @@ pub enum PushNotificationKind {
|
|||
|
||||
fn compact_content(mut content: serde_json::Value) -> Result<serde_json::Value, Error> {
|
||||
if !content.is_object() {
|
||||
return Err(Error::InvalidContent("not a JSON object".to_string()));
|
||||
return Err(Error::InvalidContent("not a JSON object".to_owned()));
|
||||
}
|
||||
|
||||
let object = content.as_object_mut().unwrap();
|
||||
|
@ -58,9 +69,7 @@ fn compact_content(mut content: serde_json::Value) -> Result<serde_json::Value,
|
|||
.get("note")
|
||||
.unwrap()
|
||||
.get("renote")
|
||||
.ok_or(Error::InvalidContent(
|
||||
"renote object is missing".to_string(),
|
||||
))?
|
||||
.ok_or(Error::InvalidContent("renote object is missing".to_owned()))?
|
||||
} else {
|
||||
object.get("note").unwrap()
|
||||
}
|
||||
|
@ -68,7 +77,7 @@ fn compact_content(mut content: serde_json::Value) -> Result<serde_json::Value,
|
|||
|
||||
if !note.is_object() {
|
||||
return Err(Error::InvalidContent(
|
||||
"(re)note is not an object".to_string(),
|
||||
"(re)note is not an object".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -90,12 +99,117 @@ fn compact_content(mut content: serde_json::Value) -> Result<serde_json::Value,
|
|||
note_object.remove("reply");
|
||||
note_object.remove("renote");
|
||||
note_object.remove("user");
|
||||
note_object.insert("text".to_string(), text.into());
|
||||
object.insert("note".to_string(), note);
|
||||
note_object.insert("text".to_owned(), text.into());
|
||||
object.insert("note".to_owned(), note);
|
||||
|
||||
Ok(serde_json::from_value(Json::Object(object.clone()))?)
|
||||
}
|
||||
|
||||
/// Returns a tuple containing the token and client name
|
||||
async fn get_mastodon_subscription_info(
|
||||
db: &DbConn,
|
||||
subscription_id: &str,
|
||||
token_id: &str,
|
||||
) -> Result<(String, String), Error> {
|
||||
let token = access_token::Entity::find()
|
||||
.filter(access_token::Column::Id.eq(token_id))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if token.is_none() {
|
||||
unsubscribe(db, subscription_id).await?;
|
||||
return Err(Error::InvalidSubscription(
|
||||
"access token not found".to_owned(),
|
||||
));
|
||||
}
|
||||
let token = token.unwrap();
|
||||
|
||||
if token.app_id.is_none() {
|
||||
unsubscribe(db, subscription_id).await?;
|
||||
return Err(Error::InvalidSubscription("no app ID".to_owned()));
|
||||
}
|
||||
let app_id = token.app_id.unwrap();
|
||||
|
||||
let client = app::Entity::find()
|
||||
.filter(app::Column::Id.eq(app_id))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if client.is_none() {
|
||||
unsubscribe(db, subscription_id).await?;
|
||||
return Err(Error::InvalidSubscription("app not found".to_owned()));
|
||||
}
|
||||
|
||||
Ok((token.token, client.unwrap().name))
|
||||
}
|
||||
|
||||
async fn encode_mastodon_payload(
|
||||
mut content: serde_json::Value,
|
||||
db: &DbConn,
|
||||
subscription: &sw_subscription::Model,
|
||||
) -> Result<String, Error> {
|
||||
let object = content
|
||||
.as_object_mut()
|
||||
.ok_or(Error::InvalidContent("not a JSON object".to_owned()))?;
|
||||
|
||||
if subscription.app_access_token_id.is_none() {
|
||||
unsubscribe(db, &subscription.id).await?;
|
||||
return Err(Error::InvalidSubscription("no access token".to_owned()));
|
||||
}
|
||||
|
||||
let (token, client_name) = get_mastodon_subscription_info(
|
||||
db,
|
||||
&subscription.id,
|
||||
subscription.app_access_token_id.as_ref().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
object.insert("access_token".to_owned(), serde_json::to_value(token)?);
|
||||
|
||||
// Some apps expect notification_id to be an integer,
|
||||
// but doesn’t break when the ID doesn’t match the rest of API.
|
||||
if [
|
||||
"IceCubesApp",
|
||||
"Mammoth",
|
||||
"feather",
|
||||
"MaserApp",
|
||||
"Metatext",
|
||||
"Feditext",
|
||||
]
|
||||
.contains(&client_name.as_str())
|
||||
{
|
||||
let timestamp = object
|
||||
.get("notification_id")
|
||||
.and_then(|id| id.as_str())
|
||||
.map(get_timestamp)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
object.insert("notification_id".to_owned(), timestamp.into());
|
||||
}
|
||||
|
||||
let res = serde_json::to_string(&content)?;
|
||||
|
||||
// Adding space paddings to the end of JSON payload to prevent
|
||||
// `esm` from adding null bytes payload which many Mastodon clients don’t support.
|
||||
// https://firefish.dev/firefish/firefish/-/merge_requests/10905#note_6733
|
||||
// not using the padding parameter directly on `res` because we want the padding to be
|
||||
// calculated based on the UTF-8 byte size of `res` instead of number of characters.
|
||||
let pad_length = match res.len() % 128 {
|
||||
127 => 127,
|
||||
n => 126 - n,
|
||||
};
|
||||
|
||||
Ok(format!("{}{:pad_length$}", res, ""))
|
||||
}
|
||||
|
||||
async fn unsubscribe(db: &DbConn, subscription_id: &str) -> Result<(), DbErr> {
|
||||
sw_subscription::Entity::delete_by_id(subscription_id)
|
||||
.exec(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_web_push_failure(
|
||||
db: &DbConn,
|
||||
err: WebPushError,
|
||||
|
@ -114,9 +228,7 @@ async fn handle_web_push_failure(
|
|||
| WebPushError::MissingCryptoKeys
|
||||
| WebPushError::InvalidCryptoKeys
|
||||
| WebPushError::InvalidResponse => {
|
||||
sw_subscription::Entity::delete_by_id(subscription_id)
|
||||
.exec(db)
|
||||
.await?;
|
||||
unsubscribe(db, subscription_id).await?;
|
||||
tracing::info!("{}; {} was unsubscribed", error_message, subscription_id);
|
||||
tracing::debug!("reason: {:#?}", err);
|
||||
}
|
||||
|
@ -157,9 +269,9 @@ pub async fn send_push_notification(
|
|||
let use_mastodon_api = matches!(kind, PushNotificationKind::Mastodon);
|
||||
|
||||
// TODO: refactoring
|
||||
let payload = if use_mastodon_api {
|
||||
// Leave the `content` as it is
|
||||
serde_json::to_string(content)?
|
||||
let mut payload = if use_mastodon_api {
|
||||
// Content generated per subscription
|
||||
"".to_owned()
|
||||
} else {
|
||||
// Format the `content` passed from the TypeScript backend
|
||||
// for Firefish push notifications
|
||||
|
@ -206,6 +318,15 @@ pub async fn send_push_notification(
|
|||
continue;
|
||||
}
|
||||
|
||||
if use_mastodon_api {
|
||||
if subscription.app_access_token_id.is_none() {
|
||||
continue;
|
||||
}
|
||||
payload = encode_mastodon_payload(content.clone(), db, subscription).await?;
|
||||
} else if subscription.app_access_token_id.is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let subscription_info = SubscriptionInfo {
|
||||
endpoint: subscription.endpoint.to_owned(),
|
||||
keys: SubscriptionKeys {
|
||||
|
@ -246,7 +367,28 @@ pub async fn send_push_notification(
|
|||
handle_web_push_failure(db, err, &subscription.id, "failed to build a payload").await?;
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = get_client()?.send(message.unwrap()).await {
|
||||
|
||||
// Ice Cubes cannot process ";rs=4096" at at the end of Encryption header
|
||||
let mut message = message.unwrap();
|
||||
|
||||
if let Some(payload) = message.payload {
|
||||
let crypto_headers: Vec<(&str, String)> = payload
|
||||
.crypto_headers
|
||||
.into_iter()
|
||||
.map(|(key, val)| match key {
|
||||
"Encryption" => (key, val.replace(";rs=4096", "")),
|
||||
_ => (key, val),
|
||||
})
|
||||
.collect();
|
||||
|
||||
message.payload = Some(WebPushPayload {
|
||||
content: payload.content,
|
||||
content_encoding: payload.content_encoding,
|
||||
crypto_headers,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(err) = get_client()?.send(message).await {
|
||||
handle_web_push_failure(db, err, &subscription.id, "failed to send").await?;
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -80,13 +80,13 @@ pub async fn publish_to_stream(
|
|||
value: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let channel = match stream {
|
||||
Stream::Internal => "internal".to_string(),
|
||||
Stream::CustomEmoji => "broadcast".to_string(),
|
||||
Stream::Internal => "internal".to_owned(),
|
||||
Stream::CustomEmoji => "broadcast".to_owned(),
|
||||
Stream::Moderation { moderator_id } => format!("adminStream:{moderator_id}"),
|
||||
Stream::User { user_id } => format!("user:{user_id}"),
|
||||
Stream::Channel { channel_id } => format!("channelStream:{channel_id}"),
|
||||
Stream::Note { note_id } => format!("noteStream:{note_id}"),
|
||||
Stream::Notes => "notesStream".to_string(),
|
||||
Stream::Notes => "notesStream".to_owned(),
|
||||
Stream::UserList { list_id } => format!("userListStream:{list_id}"),
|
||||
Stream::Main { user_id } => format!("mainStream:{user_id}"),
|
||||
Stream::Drive { user_id } => format!("driveStream:{user_id}"),
|
||||
|
@ -103,7 +103,7 @@ pub async fn publish_to_stream(
|
|||
format!(
|
||||
"{{\"type\":\"{}\",\"body\":{}}}",
|
||||
kind,
|
||||
value.unwrap_or_else(|| "null".to_string()),
|
||||
value.unwrap_or_else(|| "null".to_owned()),
|
||||
)
|
||||
} else {
|
||||
value.ok_or(Error::InvalidContent)?
|
||||
|
|
|
@ -46,7 +46,7 @@ mod unit_test {
|
|||
Err(InnerError1)
|
||||
}
|
||||
fn causes_inner_error_2() -> Result<(), InnerError2> {
|
||||
Err(InnerError2("foo".to_string()))
|
||||
Err(InnerError2("foo".to_owned()))
|
||||
}
|
||||
|
||||
fn causes_error_1() -> Result<(), ErrorVariants> {
|
||||
|
|
|
@ -58,7 +58,7 @@ pub fn get_timestamp(id: &str) -> Result<i64, InvalidIdError> {
|
|||
if let Some(n) = n {
|
||||
Ok(n as i64 + TIME_2000)
|
||||
} else {
|
||||
Err(InvalidIdError { id: id.to_string() })
|
||||
Err(InvalidIdError { id: id.to_owned() })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,21 +20,23 @@
|
|||
"format": "pnpm biome format * --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.20.5",
|
||||
"@bull-board/koa": "5.20.5",
|
||||
"@bull-board/ui": "5.20.5",
|
||||
"@bull-board/api": "5.21.0",
|
||||
"@bull-board/koa": "5.21.0",
|
||||
"@bull-board/ui": "5.21.0",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
"@koa/router": "12.0.1",
|
||||
"@ladjs/koa-views": "9.0.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@redocly/openapi-core": "1.17.1",
|
||||
"@redocly/openapi-core": "1.18.0",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"adm-zip": "0.5.14",
|
||||
"ajv": "8.16.0",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
"aws-sdk": "2.1655.0",
|
||||
"async-lock": "1.4.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"aws-sdk": "2.1659.0",
|
||||
"axios": "1.7.2",
|
||||
"backend-rs": "workspace:*",
|
||||
"blurhash": "2.0.5",
|
||||
|
@ -51,7 +53,7 @@
|
|||
"deepl-node": "1.13.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "19.0.0",
|
||||
"file-type": "19.1.1",
|
||||
"firefish-js": "workspace:*",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.0",
|
||||
|
@ -75,7 +77,6 @@
|
|||
"koa-mount": "4.0.0",
|
||||
"koa-remove-trailing-slashes": "2.0.3",
|
||||
"koa-send": "5.0.1",
|
||||
"megalodon": "workspace:*",
|
||||
"mfm-js": "0.24.0",
|
||||
"mime-types": "2.1.35",
|
||||
"msgpackr": "1.10.2",
|
||||
|
@ -93,7 +94,7 @@
|
|||
"punycode": "2.3.1",
|
||||
"pureimage": "0.4.13",
|
||||
"qrcode": "1.5.3",
|
||||
"qs": "6.12.2",
|
||||
"qs": "6.12.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"redis-semaphore": "5.6.0",
|
||||
|
@ -113,18 +114,20 @@
|
|||
"tmp": "0.2.3",
|
||||
"typeorm": "0.3.20",
|
||||
"ulid": "2.3.0",
|
||||
"unfurl.js": "6.4.0",
|
||||
"uuid": "10.0.0",
|
||||
"websocket": "1.0.35",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "0.5.5",
|
||||
"@types/async-lock": "1.4.2",
|
||||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/jsonld": "1.5.14",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.14",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/koa": "2.15.0",
|
||||
|
@ -160,7 +163,7 @@
|
|||
"@types/tmp": "0.2.6",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@types/websocket": "1.0.10",
|
||||
"@types/ws": "8.5.10",
|
||||
"@types/ws": "8.5.11",
|
||||
"cross-env": "7.0.3",
|
||||
"mocha": "10.6.0",
|
||||
"pug": "3.0.3",
|
||||
|
@ -171,7 +174,7 @@
|
|||
"tsconfig-paths": "4.2.0",
|
||||
"type-fest": "4.21.0",
|
||||
"typescript": "5.5.3",
|
||||
"webpack": "5.92.1",
|
||||
"webpack": "5.93.0",
|
||||
"ws": "8.18.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class SwSubscriptionAccessToken1709395223612
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "SwSubscriptionAccessToken1709395223612";
|
||||
|
||||
async up(queryRunner: QueryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sw_subscription" ADD COLUMN IF NOT EXISTS "appAccessTokenId" character varying(32)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sw_subscription" ADD CONSTRAINT "FK_98a1aa2db2a5253924f42f38767" FOREIGN KEY ("appAccessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_98a1aa2db2a5253924f42f3876" ON "sw_subscription" ("appAccessTokenId") `,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sw_subscription" DROP CONSTRAINT "FK_98a1aa2db2a5253924f42f38767"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sw_subscription" DROP COLUMN "appAccessTokenId"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class UserProfileMentions1711075007936 implements MigrationInterface {
|
||||
name = "UserProfileMentions1711075007936";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_profile" ADD "mentions" jsonb NOT NULL DEFAULT '[]'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_profile" DROP COLUMN "mentions"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,459 +1,459 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DropTimeZone1712425488543 implements MigrationInterface {
|
||||
public async up(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`,
|
||||
);
|
||||
public async up(_: 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`,
|
||||
// );
|
||||
}
|
||||
|
||||
public async down(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): 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`,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class ClientCredentials1713108561474 implements MigrationInterface {
|
||||
name = "ClientCredentials1713108561474";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "access_token" ALTER COLUMN "userId" DROP NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "access_token" ALTER COLUMN "userId" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddMastodonSubscriptionType1715181461692
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "AddMastodonSubscriptionType1715181461692";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "push_subscription_type" AS ENUM ('mention', 'status', 'reblog', 'follow', 'follow_request', 'favourite', 'poll', 'update', 'admin.sign_up', 'admin.report')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sw_subscription" ADD "subscriptionTypes" push_subscription_type array NOT NULL DEFAULT '{}'`,
|
||||
);
|
||||
await queryRunner.query(`
|
||||
UPDATE "sw_subscription"
|
||||
SET "subscriptionTypes" = ARRAY['mention', 'status', 'reblog', 'follow', 'follow_request', 'favourite', 'poll', 'update']::push_subscription_type[]
|
||||
WHERE "appAccessTokenId" IS NOT NULL
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "sw_subscription" DROP COLUMN "subscriptionTypes"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "push_subscription_type"`);
|
||||
}
|
||||
}
|
|
@ -229,231 +229,231 @@ export class AddBackTimezone1715351290096 implements MigrationInterface {
|
|||
);
|
||||
}
|
||||
|
||||
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`,
|
||||
);
|
||||
public async down(_: 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`,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
|
||||
import { generateUserToken, genIdAt, hashPassword } from "backend-rs";
|
||||
|
||||
async function createSystemUser(username: string, queryRunner: QueryRunner) {
|
||||
const password = uuid();
|
||||
|
||||
// Generate hash of password
|
||||
const hash = hashPassword(password);
|
||||
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
|
||||
const keyPair = await genRsaKeyPair(4096);
|
||||
|
||||
const existsQuery = await queryRunner.query(
|
||||
`SELECT "usernameLower" FROM "user" WHERE "usernameLower" = $1 AND "host" IS NULL`,
|
||||
[username.toLowerCase()],
|
||||
);
|
||||
if (existsQuery.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user" (
|
||||
"id", "createdAt", "username", "usernameLower", "host", "token",
|
||||
"isAdmin", "isLocked", "isExplorable", "isBot"
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, NULL,
|
||||
$5, FALSE, TRUE, FALSE, TRUE
|
||||
)`,
|
||||
[genIdAt(now), now, username, username.toLowerCase(), secret],
|
||||
);
|
||||
|
||||
const account = await queryRunner.query(
|
||||
`SELECT * FROM "user" WHERE "usernameLower" = $1 AND "host" IS NULL`,
|
||||
[username.toLowerCase()],
|
||||
);
|
||||
if (!account.length) {
|
||||
throw new Error("Account not found");
|
||||
}
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_keypair" ("publicKey", "privateKey", "userId")
|
||||
VALUES ($1, $2, $3)`,
|
||||
[keyPair.publicKey, keyPair.privateKey, account[0].id],
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "user_profile" ("userId", "autoAcceptFollowed", "password")
|
||||
VALUES ($1, FALSE, $2)`,
|
||||
[account[0].id, hash],
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "used_username" ("createdAt", "username")
|
||||
VALUES ($1, $2)`,
|
||||
[now, username.toLowerCase()],
|
||||
);
|
||||
}
|
||||
|
||||
export class CreateSystemActors1720618854585 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await createSystemUser("instance.actor", queryRunner);
|
||||
await createSystemUser("relay.actor", queryRunner);
|
||||
}
|
||||
|
||||
public async down(_: QueryRunner): Promise<void> {
|
||||
/* You don't need to revert this migration. */
|
||||
}
|
||||
}
|
36
packages/backend/src/misc/is-filtered.ts
Normal file
36
packages/backend/src/misc/is-filtered.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { User } from "@/models/entities/user.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { checkWordMute } from "backend-rs";
|
||||
import { Cache } from "@/misc/cache.js";
|
||||
import { UserProfiles } from "@/models/index.js";
|
||||
|
||||
const filteredNoteCache = new Cache<boolean>("filteredNote", 60 * 60 * 24);
|
||||
const mutedWordsCache = new Cache<
|
||||
Pick<UserProfile, "mutedWords" | "mutedPatterns">
|
||||
>("mutedWords", 60 * 5);
|
||||
|
||||
export async function isFiltered(
|
||||
note: Note,
|
||||
user: { id: User["id"] } | null | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!user) return false;
|
||||
const profile = await mutedWordsCache.fetch(user.id, () =>
|
||||
UserProfiles.findOneBy({ userId: user.id }).then((p) => ({
|
||||
mutedWords: p?.mutedWords ?? [],
|
||||
mutedPatterns: p?.mutedPatterns ?? [],
|
||||
})),
|
||||
);
|
||||
|
||||
if (
|
||||
!profile ||
|
||||
(profile.mutedPatterns.length < 1 && profile.mutedWords.length < 1)
|
||||
)
|
||||
return false;
|
||||
const ts = (note.updatedAt ?? note.createdAt) as Date | string;
|
||||
const identifier =
|
||||
(typeof ts === "string" ? new Date(ts) : ts)?.getTime() ?? "0";
|
||||
return filteredNoteCache.fetch(`${note.id}:${identifier}:${user.id}`, () =>
|
||||
checkWordMute(note, profile.mutedWords, profile.mutedPatterns),
|
||||
);
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
import type { Packed } from "./schema.js";
|
||||
|
||||
type NoteWithUserHost = { user: { host: string | null } | null };
|
||||
|
||||
export function isInstanceMuted(
|
||||
note: Packed<"Note">,
|
||||
note: NoteWithUserHost & {
|
||||
reply: NoteWithUserHost | null;
|
||||
renote: NoteWithUserHost | null;
|
||||
},
|
||||
mutedInstances: Set<string>,
|
||||
): boolean {
|
||||
if (mutedInstances.has(note?.user?.host ?? "")) return true;
|
||||
|
|
|
@ -14,7 +14,7 @@ const cache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12);
|
|||
/**
|
||||
* 添付用絵文字情報
|
||||
*/
|
||||
type PopulatedEmoji = {
|
||||
export type PopulatedEmoji = {
|
||||
name: string;
|
||||
url: string;
|
||||
width: number | null;
|
||||
|
|
90
packages/backend/src/misc/search.ts
Normal file
90
packages/backend/src/misc/search.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { Notes } from "@/models/index.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
|
||||
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import type { SelectQueryBuilder } from "typeorm";
|
||||
|
||||
export interface SearchParams {
|
||||
withFilesOnly: boolean;
|
||||
limit: number;
|
||||
myId: string | undefined;
|
||||
sinceId: string | undefined;
|
||||
untilId: string | undefined;
|
||||
sinceDate: number | undefined;
|
||||
untilDate: number | undefined;
|
||||
userId: string | null | undefined;
|
||||
channelId: string | null;
|
||||
host: string | null | undefined;
|
||||
}
|
||||
|
||||
export async function searchNotes(
|
||||
params: SearchParams,
|
||||
modifier?: (query: SelectQueryBuilder<Note>) => void,
|
||||
): Promise<Note[]> {
|
||||
const query = makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
params.sinceId ?? undefined,
|
||||
params.untilId ?? undefined,
|
||||
params.sinceDate ?? undefined,
|
||||
params.untilDate ?? undefined,
|
||||
);
|
||||
modifier?.(query);
|
||||
|
||||
if (params.userId != null) {
|
||||
query.andWhere("note.userId = :userId", { userId: params.userId });
|
||||
}
|
||||
|
||||
if (params.channelId != null) {
|
||||
query.andWhere("note.channelId = :channelId", {
|
||||
channelId: params.channelId,
|
||||
});
|
||||
}
|
||||
|
||||
query.innerJoinAndSelect("note.user", "user");
|
||||
|
||||
// "from: me": search all (public, home, followers, specified) my posts
|
||||
// otherwise: search public indexable posts only
|
||||
if (params.userId == null || params.userId !== params.myId) {
|
||||
query
|
||||
.andWhere("note.visibility = 'public'")
|
||||
.andWhere("user.isIndexable = TRUE");
|
||||
}
|
||||
|
||||
if (params.userId != null) {
|
||||
query.andWhere("note.userId = :userId", { userId: params.userId });
|
||||
}
|
||||
|
||||
if (params.host === null) {
|
||||
// search local notes only
|
||||
query.andWhere("note.userHost IS NULL");
|
||||
}
|
||||
if (params.host != null) {
|
||||
query.andWhere("note.userHost = :userHost", { userHost: params.host });
|
||||
}
|
||||
|
||||
if (params.withFilesOnly) {
|
||||
query.andWhere("note.fileIds != '{}'");
|
||||
}
|
||||
|
||||
query
|
||||
.leftJoinAndSelect("user.avatar", "avatar")
|
||||
.leftJoinAndSelect("user.banner", "banner")
|
||||
.leftJoinAndSelect("note.reply", "reply")
|
||||
.leftJoinAndSelect("note.renote", "renote")
|
||||
.leftJoinAndSelect("reply.user", "replyUser")
|
||||
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
|
||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
||||
|
||||
const me = params.myId != null ? { id: params.myId } : null;
|
||||
|
||||
generateVisibilityQuery(query, me);
|
||||
if (params.myId != null) generateMutedUserQuery(query, { id: params.myId });
|
||||
if (params.myId != null) generateBlockedUserQuery(query, { id: params.myId });
|
||||
|
||||
return await query.take(params.limit).getMany();
|
||||
}
|
|
@ -46,8 +46,11 @@ export class AccessToken {
|
|||
public hash: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: User["id"];
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public userId: User["id"] | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -312,9 +312,11 @@ export class Note {
|
|||
}
|
||||
}
|
||||
|
||||
export type IMentionedRemoteUsers = {
|
||||
export type IMentionedRemoteUser = {
|
||||
uri: string;
|
||||
url?: string;
|
||||
username: string;
|
||||
host: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type IMentionedRemoteUsers = IMentionedRemoteUser[];
|
||||
|
|
|
@ -9,6 +9,23 @@ import {
|
|||
} from "typeorm";
|
||||
import { User } from "./user.js";
|
||||
import { id } from "../id.js";
|
||||
import { AccessToken } from "./access-token.js";
|
||||
|
||||
// for Mastodon push notifications
|
||||
const pushSubscriptionTypes = [
|
||||
"mention",
|
||||
"status",
|
||||
"reblog",
|
||||
"follow",
|
||||
"follow_request",
|
||||
"favourite",
|
||||
"poll",
|
||||
"update",
|
||||
"admin.sign_up",
|
||||
"admin.report",
|
||||
] as const;
|
||||
|
||||
type pushSubscriptionType = (typeof pushSubscriptionTypes)[number];
|
||||
|
||||
@Entity()
|
||||
export class SwSubscription {
|
||||
|
@ -42,11 +59,40 @@ export class SwSubscription {
|
|||
})
|
||||
public sendReadMessage: boolean;
|
||||
|
||||
/**
|
||||
* Type of subscription, used for Mastodon API notifications.
|
||||
* Empty for Misskey notifications.
|
||||
*/
|
||||
@Column({
|
||||
type: "enum",
|
||||
enum: pushSubscriptionTypes,
|
||||
array: true,
|
||||
default: "{}",
|
||||
})
|
||||
public subscriptionTypes: pushSubscriptionType[];
|
||||
|
||||
/**
|
||||
* App notification app, used for Mastodon API notifications
|
||||
*/
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public appAccessTokenId: AccessToken["id"] | null;
|
||||
|
||||
//#region Relations
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: Relation<User>;
|
||||
|
||||
@ManyToOne(() => AccessToken, {
|
||||
onDelete: "CASCADE",
|
||||
nullable: true,
|
||||
})
|
||||
@JoinColumn()
|
||||
public appAccessToken: Relation<AccessToken | null>;
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ffVisibility, notificationTypes } from "@/types.js";
|
|||
import { id } from "../id.js";
|
||||
import { User } from "./user.js";
|
||||
import { Page } from "./page.js";
|
||||
import type { IMentionedRemoteUsers } from "./note.js";
|
||||
|
||||
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
|
||||
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
|
||||
|
@ -50,6 +51,11 @@ export class UserProfile {
|
|||
verified?: boolean;
|
||||
}[];
|
||||
|
||||
@Column("jsonb", {
|
||||
default: [],
|
||||
})
|
||||
public mentions: IMentionedRemoteUsers;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 32,
|
||||
nullable: true,
|
||||
|
|
|
@ -37,7 +37,6 @@ import { AppRepository } from "./repositories/app.js";
|
|||
import { FollowingRepository } from "./repositories/following.js";
|
||||
import { AbuseUserReportRepository } from "./repositories/abuse-user-report.js";
|
||||
import { AuthSessionRepository } from "./repositories/auth-session.js";
|
||||
import { UserProfile } from "./entities/user-profile.js";
|
||||
import { AttestationChallenge } from "./entities/attestation-challenge.js";
|
||||
import { UserSecurityKey } from "./entities/user-security-key.js";
|
||||
import { HashtagRepository } from "./repositories/hashtag.js";
|
||||
|
@ -67,6 +66,7 @@ import { Webhook } from "./entities/webhook.js";
|
|||
import { UserIp } from "./entities/user-ip.js";
|
||||
import { NoteFileRepository } from "./repositories/note-file.js";
|
||||
import { NoteEditRepository } from "./repositories/note-edit.js";
|
||||
import { UserProfileRepository } from "./repositories/user-profile.js";
|
||||
|
||||
export const Announcements = db.getRepository(Announcement);
|
||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||
|
@ -82,7 +82,7 @@ export const NoteUnreads = db.getRepository(NoteUnread);
|
|||
export const Polls = db.getRepository(Poll);
|
||||
export const PollVotes = db.getRepository(PollVote);
|
||||
export const Users = UserRepository;
|
||||
export const UserProfiles = db.getRepository(UserProfile);
|
||||
export const UserProfiles = UserProfileRepository;
|
||||
export const UserKeypairs = db.getRepository(UserKeypair);
|
||||
export const UserPendings = db.getRepository(UserPending);
|
||||
export const AttestationChallenges = db.getRepository(AttestationChallenge);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { config } from "@/config.js";
|
|||
import { query, appendQuery } from "@/prelude/url.js";
|
||||
import { Users, DriveFolders } from "../index.js";
|
||||
import { deepClone } from "@/misc/clone.js";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
|
||||
type PackOptions = {
|
||||
detail?: boolean;
|
||||
|
@ -221,4 +222,33 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
|||
);
|
||||
return items.filter((x): x is Packed<"DriveFile"> => x != null);
|
||||
},
|
||||
|
||||
async getFinalUrl(url: string): Promise<string> {
|
||||
if (!config.proxyRemoteFiles) return url;
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://")) return url;
|
||||
if (url.startsWith(`${config.url}/files`)) return url;
|
||||
if (url.startsWith(`${config.url}/static-assets`)) return url;
|
||||
if (url.startsWith(`${config.url}/identicon`)) return url;
|
||||
if (url.startsWith(`${config.url}/avatar`)) return url;
|
||||
|
||||
const meta = await fetchMeta();
|
||||
const baseUrl = meta
|
||||
? meta.objectStorageBaseUrl ??
|
||||
`${meta.objectStorageUseSsl ? "https" : "http"}://${
|
||||
meta.objectStorageEndpoint
|
||||
}${meta.objectStoragePort ? `:${meta.objectStoragePort}` : ""}/${
|
||||
meta.objectStorageBucket
|
||||
}`
|
||||
: null;
|
||||
if (baseUrl !== null && url.startsWith(baseUrl)) return url;
|
||||
|
||||
return `${config.url}/proxy/${encodeURIComponent(
|
||||
new URL(url).pathname,
|
||||
)}?${query({ url: url })}`;
|
||||
},
|
||||
|
||||
async getFinalUrlMaybe(url?: string | null): Promise<string | null> {
|
||||
if (url == null) return null;
|
||||
return this.getFinalUrl(url);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
Followings,
|
||||
Polls,
|
||||
Channels,
|
||||
UserProfiles,
|
||||
Notes,
|
||||
} from "../index.js";
|
||||
import type { Packed } from "@/misc/schema.js";
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
} from "@/misc/populate-emojis.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
||||
import { config } from "@/config.js";
|
||||
|
||||
export async function populatePoll(note: Note, meId: User["id"] | null) {
|
||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
|
@ -150,6 +152,29 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
return true;
|
||||
},
|
||||
|
||||
async mentionedRemoteUsers(note: Note): Promise<string | undefined> {
|
||||
if (note.mentions?.length) {
|
||||
const mentionedUserIds = [...new Set(note.mentions)].sort();
|
||||
const mentionedUsers = await Users.findBy({
|
||||
id: In(mentionedUserIds),
|
||||
});
|
||||
const userProfiles = await UserProfiles.findBy({
|
||||
userId: In(mentionedUserIds),
|
||||
});
|
||||
return JSON.stringify(
|
||||
mentionedUsers.map((u) => ({
|
||||
username: u.username,
|
||||
host: u.host ?? config.host,
|
||||
uri: u.uri ?? `${config.url}/users/${u.id}`,
|
||||
url:
|
||||
userProfiles.find((p) => p.userId === u.id)?.url ??
|
||||
`${config.url}/@${u.username}`,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
async pack(
|
||||
src: Note["id"] | Note,
|
||||
me?: { id: User["id"] } | null | undefined,
|
||||
|
@ -288,6 +313,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
}
|
||||
: {}),
|
||||
lang: note.lang,
|
||||
mentionedRemoteUsers: this.mentionedRemoteUsers(note),
|
||||
});
|
||||
|
||||
if (
|
||||
|
|
87
packages/backend/src/models/repositories/user-profile.ts
Normal file
87
packages/backend/src/models/repositories/user-profile.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { db } from "@/db/postgre.js";
|
||||
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import mfm from "mfm-js";
|
||||
import { extractMentions } from "@/misc/extract-mentions.js";
|
||||
import {
|
||||
type ProfileMention,
|
||||
resolveMentionToUserAndProfile,
|
||||
} from "@/remote/resolve-user.js";
|
||||
import type {
|
||||
IMentionedRemoteUser,
|
||||
IMentionedRemoteUsers,
|
||||
} from "@/models/entities/note.js";
|
||||
import { unique } from "@/prelude/array.js";
|
||||
import { config } from "@/config.js";
|
||||
import { Mutex, Semaphore } from "async-mutex";
|
||||
|
||||
const queue = new Semaphore(5);
|
||||
|
||||
export const UserProfileRepository = db.getRepository(UserProfile).extend({
|
||||
// We must never await this without promiseEarlyReturn, otherwise giant webring-style profile mention trees will cause the queue to stop working
|
||||
async updateMentions(
|
||||
id: UserProfile["userId"],
|
||||
limiter: RecursionLimiter = new RecursionLimiter(),
|
||||
) {
|
||||
const profile = await this.findOneBy({ userId: id });
|
||||
if (!profile) return;
|
||||
const tokens: mfm.MfmNode[] = [];
|
||||
|
||||
if (profile.description) tokens.push(...mfm.parse(profile.description));
|
||||
if (profile.fields.length > 0)
|
||||
tokens.push(
|
||||
...profile.fields.flatMap((p) =>
|
||||
mfm.parse(p.value).concat(mfm.parse(p.name)),
|
||||
),
|
||||
);
|
||||
|
||||
return queue.runExclusive(async () => {
|
||||
const partial = {
|
||||
mentions: await populateMentions(tokens, profile.userHost, limiter),
|
||||
};
|
||||
return UserProfileRepository.update(profile.userId, partial);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
async function populateMentions(
|
||||
tokens: mfm.MfmNode[],
|
||||
objectHost: string | null,
|
||||
limiter: RecursionLimiter,
|
||||
): Promise<IMentionedRemoteUsers> {
|
||||
const mentions = extractMentions(tokens);
|
||||
const resolved = await Promise.all(
|
||||
mentions.map((m) =>
|
||||
resolveMentionToUserAndProfile(m.username, m.host, objectHost, limiter),
|
||||
),
|
||||
);
|
||||
const remote = resolved.filter(
|
||||
(p): p is ProfileMention =>
|
||||
!!p &&
|
||||
p.data.host !== config.host &&
|
||||
(p.data.host !== null || objectHost !== null),
|
||||
);
|
||||
const res = remote.map((m) => {
|
||||
return {
|
||||
uri: m.user.uri,
|
||||
url: m.profile?.url ?? undefined,
|
||||
username: m.data.username,
|
||||
host: m.data.host,
|
||||
} as IMentionedRemoteUser;
|
||||
});
|
||||
|
||||
return unique(res);
|
||||
}
|
||||
|
||||
export class RecursionLimiter {
|
||||
private counter;
|
||||
private mutex = new Mutex();
|
||||
constructor(count = 10) {
|
||||
this.counter = count;
|
||||
}
|
||||
|
||||
public shouldContinue(): Promise<boolean> {
|
||||
return this.mutex.runExclusive(() => {
|
||||
return this.counter-- > 0;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -51,6 +51,17 @@ export function unique<T>(xs: T[]): T[] {
|
|||
return [...new Set(xs)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters an array of elements based on unique outputs of a key function
|
||||
*/
|
||||
export function uniqBy<T, U>(a: T[], key: (elm: T) => U): T[] {
|
||||
const seen = new Set<U>();
|
||||
return a.filter((item) => {
|
||||
const k = key(item);
|
||||
return seen.has(k) ? false : seen.add(k);
|
||||
});
|
||||
}
|
||||
|
||||
export function sum(xs: number[]): number {
|
||||
return xs.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
@ -150,3 +161,7 @@ export function toArray<T>(x: T | T[] | undefined): T[] {
|
|||
export function toSingle<T>(x: T | T[] | undefined): T | undefined {
|
||||
return Array.isArray(x) ? x[0] : x;
|
||||
}
|
||||
|
||||
export function toSingleLast<T>(x: T | T[] | undefined): T | undefined {
|
||||
return Array.isArray(x) ? x.at(-1) : x;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,27 @@ export type Promiseable<T> = {
|
|||
[K in keyof T]: Promise<T[K]> | T[K];
|
||||
};
|
||||
|
||||
export async function awaitAll<T>(obj: Promiseable<T>): Promise<T> {
|
||||
type RecursiveResolvePromise<U> = U extends Date
|
||||
? U
|
||||
: U extends Array<infer V>
|
||||
? Array<ResolvedPromise<V>>
|
||||
: U extends object
|
||||
? { [key in keyof U]: ResolvedPromise<U[key]> }
|
||||
: U;
|
||||
|
||||
type ResolvedPromise<T> = T extends Promise<infer U>
|
||||
? RecursiveResolvePromise<U>
|
||||
: RecursiveResolvePromise<T>;
|
||||
|
||||
export type OuterPromise<T> = Promise<{
|
||||
[K in keyof T]: ResolvedPromise<T[K]>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Resolve all promises in the object recursively,
|
||||
* and return a promise that resolves to the object with all promises resolved.
|
||||
*/
|
||||
export async function awaitAll<T>(obj: Promiseable<T>): OuterPromise<T> {
|
||||
const target = {} as T;
|
||||
const keys = unsafeCast<(keyof T)[]>(Object.keys(obj));
|
||||
const values = Object.values(obj) as any[];
|
||||
|
@ -21,5 +41,5 @@ export async function awaitAll<T>(obj: Promiseable<T>): Promise<T> {
|
|||
target[keys[i]] = resolvedValues[i];
|
||||
}
|
||||
|
||||
return target;
|
||||
return target as OuterPromise<T>;
|
||||
}
|
||||
|
|
13
packages/backend/src/prelude/promise.ts
Normal file
13
packages/backend/src/prelude/promise.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Returns T if promise settles before timeout,
|
||||
* otherwise returns void, finishing execution in the background.
|
||||
*/
|
||||
export async function promiseEarlyReturn<T>(
|
||||
promise: Promise<T>,
|
||||
after: number,
|
||||
): Promise<T | void> {
|
||||
const timer: Promise<void> = new Promise((res) =>
|
||||
setTimeout(() => res(undefined), after),
|
||||
);
|
||||
return Promise.race([promise, timer]);
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { config } from "@/config.js";
|
||||
import type { ILocalUser } from "@/models/entities/user.js";
|
||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||
import {
|
||||
extractHost,
|
||||
getInternalActor,
|
||||
isAllowedServer,
|
||||
isBlockedServer,
|
||||
isSelfHost,
|
||||
|
@ -112,7 +112,7 @@ export default class Resolver {
|
|||
}
|
||||
|
||||
if (!this.user) {
|
||||
this.user = await getInstanceActor();
|
||||
this.user = await getInternalActor("instance");
|
||||
}
|
||||
|
||||
apLogger.info(
|
||||
|
@ -158,7 +158,7 @@ export default class Resolver {
|
|||
if (!parsed.local) throw new Error("resolveLocal: not local");
|
||||
|
||||
switch (parsed.type) {
|
||||
case "notes":
|
||||
case "notes": {
|
||||
const note = await Notes.findOneByOrFail({ id: parsed.id });
|
||||
if (parsed.rest === "activity") {
|
||||
// this refers to the create activity and not the note itself
|
||||
|
@ -166,20 +166,24 @@ export default class Resolver {
|
|||
} else {
|
||||
return renderNote(note);
|
||||
}
|
||||
case "users":
|
||||
}
|
||||
case "users": {
|
||||
const user = await Users.findOneByOrFail({ id: parsed.id });
|
||||
return await renderPerson(user as ILocalUser);
|
||||
case "questions":
|
||||
}
|
||||
case "questions": {
|
||||
// Polls are indexed by the note they are attached to.
|
||||
const [pollNote, poll] = await Promise.all([
|
||||
Notes.findOneByOrFail({ id: parsed.id }),
|
||||
Polls.findOneByOrFail({ noteId: parsed.id }),
|
||||
]);
|
||||
return await renderQuestion({ id: pollNote.userId }, pollNote, poll);
|
||||
case "likes":
|
||||
}
|
||||
case "likes": {
|
||||
const reaction = await NoteReactions.findOneByOrFail({ id: parsed.id });
|
||||
return renderActivity(renderLike(reaction, { uri: null }));
|
||||
case "follows":
|
||||
}
|
||||
case "follows": {
|
||||
// if rest is a <followee id>
|
||||
if (parsed.rest != null && /^\w+$/.test(parsed.rest)) {
|
||||
const [follower, followee] = await Promise.all(
|
||||
|
@ -207,6 +211,7 @@ export default class Resolver {
|
|||
throw new Error("resolveLocal: invalid follow URI");
|
||||
}
|
||||
return renderActivity(renderFollow(follower, followee, url));
|
||||
}
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
||||
}
|
||||
|
|
|
@ -3,21 +3,53 @@ import chalk from "chalk";
|
|||
import { IsNull } from "typeorm";
|
||||
import { config } from "@/config.js";
|
||||
import type { User, IRemoteUser } from "@/models/entities/user.js";
|
||||
import { Users } from "@/models/index.js";
|
||||
import { Cache } from "@/misc/cache.js";
|
||||
import { UserProfiles, Users } from "@/models/index.js";
|
||||
import { toPuny } from "backend-rs";
|
||||
import webFinger from "./webfinger.js";
|
||||
import { createPerson, updatePerson } from "./activitypub/models/person.js";
|
||||
import { remoteLogger } from "./logger.js";
|
||||
import { inspect } from "node:util";
|
||||
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { RecursionLimiter } from "@/models/repositories/user-profile.js";
|
||||
import { promiseEarlyReturn } from "@/prelude/promise.js";
|
||||
import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
||||
|
||||
const logger = remoteLogger.createSubLogger("resolve-user");
|
||||
const localUsernameCache = new Cache<string | null>(
|
||||
"localUserNameCapitalization",
|
||||
60 * 60 * 24,
|
||||
);
|
||||
const profileMentionCache = new Cache<ProfileMention | null>(
|
||||
"resolveProfileMentions",
|
||||
60 * 60,
|
||||
);
|
||||
|
||||
export type ProfileMention = {
|
||||
user: User;
|
||||
profile: UserProfile | null;
|
||||
data: {
|
||||
username: string;
|
||||
host: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
type refreshType =
|
||||
| "refresh"
|
||||
| "refresh-in-background"
|
||||
| "refresh-timeout-1500ms"
|
||||
| "no-refresh";
|
||||
|
||||
export async function resolveUser(
|
||||
username: string,
|
||||
host: string | null,
|
||||
refresh: refreshType = "refresh",
|
||||
limiter: RecursionLimiter = new RecursionLimiter(),
|
||||
): Promise<User> {
|
||||
const usernameLower = username.toLowerCase();
|
||||
|
||||
// Return local user if host part is empty
|
||||
|
||||
if (host == null) {
|
||||
logger.info(`return local user: ${usernameLower}`);
|
||||
return await Users.findOneBy({ usernameLower, host: IsNull() }).then(
|
||||
|
@ -33,7 +65,9 @@ export async function resolveUser(
|
|||
|
||||
host = toPuny(host);
|
||||
|
||||
if (config.host === host) {
|
||||
// Also return local user if host part is specified but referencing the local instance
|
||||
|
||||
if (config.host === host || config.host === host) {
|
||||
logger.info(`return local user: ${usernameLower}`);
|
||||
return await Users.findOneBy({ usernameLower, host: IsNull() }).then(
|
||||
(u) => {
|
||||
|
@ -46,24 +80,63 @@ export async function resolveUser(
|
|||
);
|
||||
}
|
||||
|
||||
const user = (await Users.findOneBy({
|
||||
// Check if remote user is already in the database
|
||||
|
||||
let user = (await Users.findOneBy({
|
||||
usernameLower,
|
||||
host,
|
||||
})) as IRemoteUser | null;
|
||||
|
||||
const acctLower = `${usernameLower}@${host}`;
|
||||
|
||||
if (user == null) {
|
||||
const self = await resolveSelf(acctLower);
|
||||
// If not, look up the user on the remote server
|
||||
|
||||
logger.info(`return new remote user: ${chalk.magenta(acctLower)}`);
|
||||
return await createPerson(self.href);
|
||||
if (user == null) {
|
||||
// Run WebFinger
|
||||
const fingerRes = await resolveUserWebFinger(acctLower);
|
||||
const finalAcct = subjectToAcct(fingerRes.subject);
|
||||
const finalAcctLower = finalAcct.toLowerCase();
|
||||
const m = finalAcct.match(/^([^@]+)@(.*)/);
|
||||
const subjectHost = m ? m[2] : undefined;
|
||||
|
||||
// If subject is different, we're dealing with a split domain setup (that's already been validated by resolveUserWebFinger)
|
||||
if (acctLower !== finalAcctLower) {
|
||||
logger.info("re-resolving split domain redirect user...");
|
||||
const m = finalAcct.match(/^([^@]+)@(.*)/);
|
||||
if (m) {
|
||||
// Re-check if we already have the user in the database post-redirect
|
||||
user = (await Users.findOneBy({
|
||||
usernameLower: usernameLower,
|
||||
host: subjectHost,
|
||||
})) as IRemoteUser | null;
|
||||
|
||||
// If yes, return existing user
|
||||
if (user != null) {
|
||||
logger.info(
|
||||
`return existing remote user: ${chalk.magenta(finalAcctLower)}`,
|
||||
);
|
||||
return user;
|
||||
}
|
||||
// Otherwise create and return new user
|
||||
else {
|
||||
logger.info(
|
||||
`return new remote user: ${chalk.magenta(finalAcctLower)}`,
|
||||
);
|
||||
return await createPerson(fingerRes.self.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not a split domain setup, so we can simply create and return the new user
|
||||
logger.info(`return new remote user: ${chalk.magenta(finalAcctLower)}`);
|
||||
return await createPerson(fingerRes.self.href);
|
||||
}
|
||||
|
||||
// If user information is out of date, return it by starting over from WebFilger
|
||||
// If user information is out of date, return it by starting over from WebFinger
|
||||
if (
|
||||
user.lastFetchedAt == null ||
|
||||
Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24
|
||||
(refresh === "refresh" || refresh === "refresh-timeout-1500ms") &&
|
||||
(user.lastFetchedAt == null ||
|
||||
Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24)
|
||||
) {
|
||||
// Prevent multiple attempts to connect to unconnected instances, update before each attempt to prevent subsequent similar attempts
|
||||
await Users.update(user.id, {
|
||||
|
@ -71,17 +144,17 @@ export async function resolveUser(
|
|||
});
|
||||
|
||||
logger.info(`try resync: ${acctLower}`);
|
||||
const self = await resolveSelf(acctLower);
|
||||
const fingerRes = await resolveUserWebFinger(acctLower);
|
||||
|
||||
if (user.uri !== self.href) {
|
||||
if (user.uri !== fingerRes.self.href) {
|
||||
// if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping.
|
||||
logger.info(`uri missmatch: ${acctLower}`);
|
||||
logger.info(
|
||||
`recovery missmatch uri for (username=${username}, host=${host}) from ${user.uri} to ${self.href}`,
|
||||
`recovery mismatch uri for (username=${username}, host=${host}) from ${user.uri} to ${fingerRes.self.href}`,
|
||||
);
|
||||
|
||||
// validate uri
|
||||
const uri = new URL(self.href);
|
||||
const uri = new URL(fingerRes.self.href);
|
||||
if (uri.hostname !== host) {
|
||||
throw new Error("Invalid uri");
|
||||
}
|
||||
|
@ -92,23 +165,60 @@ export async function resolveUser(
|
|||
host: host,
|
||||
},
|
||||
{
|
||||
uri: self.href,
|
||||
uri: fingerRes.self.href,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
logger.info(`uri is fine: ${acctLower}`);
|
||||
}
|
||||
|
||||
await updatePerson(self.href);
|
||||
const finalAcct = subjectToAcct(fingerRes.subject);
|
||||
const finalAcctLower = finalAcct.toLowerCase();
|
||||
const m = finalAcct.match(/^([^@]+)@(.*)/);
|
||||
const finalHost = m ? m[2] : null;
|
||||
|
||||
logger.info(`return resynced remote user: ${acctLower}`);
|
||||
return await Users.findOneBy({ uri: self.href }).then((u) => {
|
||||
// Update user.host if we're dealing with an account that's part of a split domain setup that hasn't been fixed yet
|
||||
if (m && user.host !== finalHost) {
|
||||
logger.info(
|
||||
`updating user host to subject acct host: ${user.host} -> ${finalHost}`,
|
||||
);
|
||||
await Users.update(
|
||||
{
|
||||
usernameLower,
|
||||
host: user.host,
|
||||
},
|
||||
{
|
||||
host: finalHost,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (refresh === "refresh") {
|
||||
await updatePerson(fingerRes.self.href);
|
||||
logger.info(`return resynced remote user: ${finalAcctLower}`);
|
||||
} else if (refresh === "refresh-timeout-1500ms") {
|
||||
const res = await promiseEarlyReturn(
|
||||
updatePerson(fingerRes.self.href),
|
||||
1500,
|
||||
);
|
||||
logger.info(`return possibly resynced remote user: ${finalAcctLower}`);
|
||||
}
|
||||
|
||||
return await Users.findOneBy({ uri: fingerRes.self.href }).then((u) => {
|
||||
if (u == null) {
|
||||
throw new Error("user not found");
|
||||
} else {
|
||||
return u;
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
refresh === "refresh-in-background" &&
|
||||
(user.lastFetchedAt == null ||
|
||||
Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24)
|
||||
) {
|
||||
// Run the refresh in the background
|
||||
// noinspection ES6MissingAwait
|
||||
resolveUser(username, host, "refresh", limiter);
|
||||
}
|
||||
|
||||
logger.info(`return existing remote user: ${acctLower}`);
|
||||
|
@ -136,3 +246,185 @@ async function resolveSelf(acctLower: string) {
|
|||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
async function getLocalUsernameCached(
|
||||
username: string,
|
||||
): Promise<string | null> {
|
||||
return localUsernameCache.fetch(username.toLowerCase(), () =>
|
||||
Users.findOneBy({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: IsNull(),
|
||||
}).then((p) => (p ? p.username : null)),
|
||||
);
|
||||
}
|
||||
|
||||
export function getMentionFallbackUri(
|
||||
username: string,
|
||||
host: string | null,
|
||||
objectHost: string | null,
|
||||
): string {
|
||||
let fallback = `${config.url}/@${username}`;
|
||||
if (host !== null && host !== config.host) fallback += `@${host}`;
|
||||
else if (
|
||||
objectHost !== null &&
|
||||
objectHost !== config.host &&
|
||||
host !== config.host
|
||||
)
|
||||
fallback += `@${objectHost}`;
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export async function resolveMentionFromCache(
|
||||
username: string,
|
||||
host: string | null,
|
||||
objectHost: string | null,
|
||||
cache: IMentionedRemoteUsers,
|
||||
): Promise<{
|
||||
username: string;
|
||||
href: string;
|
||||
host: string;
|
||||
isLocal: boolean;
|
||||
} | null> {
|
||||
const isLocal =
|
||||
(host === null && objectHost === null) || host === config.host;
|
||||
if (isLocal) {
|
||||
const finalUsername = await getLocalUsernameCached(username);
|
||||
if (finalUsername === null) return null;
|
||||
username = finalUsername;
|
||||
}
|
||||
|
||||
const fallback = getMentionFallbackUri(username, host, objectHost);
|
||||
const cached = cache.find(
|
||||
(r) =>
|
||||
r.username.toLowerCase() === username.toLowerCase() &&
|
||||
r.host === (host ?? objectHost),
|
||||
);
|
||||
const href = cached?.url ?? cached?.uri;
|
||||
if (cached && href != null)
|
||||
return {
|
||||
username: cached.username,
|
||||
href: href,
|
||||
isLocal,
|
||||
host: cached.host,
|
||||
};
|
||||
if (isLocal)
|
||||
return { username: username, href: fallback, isLocal, host: config.host };
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function resolveMentionToUserAndProfile(
|
||||
username: string,
|
||||
host: string | null,
|
||||
objectHost: string | null,
|
||||
limiter: RecursionLimiter,
|
||||
) {
|
||||
return profileMentionCache.fetch(
|
||||
`${username}@${host ?? objectHost}`,
|
||||
async () => {
|
||||
try {
|
||||
const user = await resolveUser(
|
||||
username,
|
||||
host ?? objectHost,
|
||||
"no-refresh",
|
||||
limiter,
|
||||
);
|
||||
const profile = await UserProfiles.findOneBy({ userId: user.id });
|
||||
const data = { username, host: host ?? objectHost };
|
||||
|
||||
return { user, profile, data };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveUserWebFinger(
|
||||
acctLower: string,
|
||||
recurse = true,
|
||||
): Promise<{
|
||||
subject: string;
|
||||
self: {
|
||||
href: string;
|
||||
rel?: string;
|
||||
};
|
||||
}> {
|
||||
logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
|
||||
const fingerRes = await webFinger(acctLower).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${
|
||||
e.statusCode || e.message
|
||||
}`,
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to WebFinger for ${acctLower}: ${e.statusCode || e.message}`,
|
||||
);
|
||||
});
|
||||
const self = fingerRes.links.find(
|
||||
(link) => link.rel != null && link.rel.toLowerCase() === "self",
|
||||
);
|
||||
if (!self) {
|
||||
logger.error(
|
||||
`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`,
|
||||
);
|
||||
throw new Error("self link not found");
|
||||
}
|
||||
if (`${acctToSubject(acctLower)}` !== normalizeSubject(fingerRes.subject)) {
|
||||
logger.info(
|
||||
`acct subject mismatch (${acctToSubject(
|
||||
acctLower,
|
||||
)} !== ${normalizeSubject(
|
||||
fingerRes.subject,
|
||||
)}), possible split domain deployment detected, repeating webfinger`,
|
||||
);
|
||||
if (!recurse) {
|
||||
logger.error(
|
||||
"split domain verification failed (recurse limit reached), aborting",
|
||||
);
|
||||
throw new Error(
|
||||
"split domain verification failed (recurse limit reached), aborting",
|
||||
);
|
||||
}
|
||||
const initialAcct = subjectToAcct(fingerRes.subject);
|
||||
const initialAcctLower = initialAcct.toLowerCase();
|
||||
const splitFingerRes = await resolveUserWebFinger(initialAcctLower, false);
|
||||
const finalAcct = subjectToAcct(splitFingerRes.subject);
|
||||
const finalAcctLower = finalAcct.toLowerCase();
|
||||
if (initialAcct !== finalAcct) {
|
||||
logger.error(
|
||||
"split domain verification failed (subject mismatch), aborting",
|
||||
);
|
||||
throw new Error(
|
||||
"split domain verification failed (subject mismatch), aborting",
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`split domain configuration detected: ${acctLower} -> ${finalAcctLower}`,
|
||||
);
|
||||
|
||||
return splitFingerRes;
|
||||
}
|
||||
|
||||
return {
|
||||
subject: fingerRes.subject,
|
||||
self: self,
|
||||
};
|
||||
}
|
||||
|
||||
function subjectToAcct(subject: string): string {
|
||||
if (!subject.startsWith("acct:")) {
|
||||
logger.error("Subject isnt a valid acct");
|
||||
throw "Subject isnt a valid acct";
|
||||
}
|
||||
return subject.substring(5);
|
||||
}
|
||||
|
||||
function acctToSubject(acct: string): string {
|
||||
return normalizeSubject(`acct:${acct}`);
|
||||
}
|
||||
|
||||
function normalizeSubject(subject: string): string {
|
||||
return subject.toLowerCase();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js";
|
|||
import { renderPerson } from "@/remote/activitypub/renderer/person.js";
|
||||
import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
|
||||
import { inbox as processInbox } from "@/queue/index.js";
|
||||
import { fetchMeta, isSelfHost } from "backend-rs";
|
||||
import { fetchMeta, getInternalActor, isSelfHost } from "backend-rs";
|
||||
import {
|
||||
Notes,
|
||||
Users,
|
||||
|
@ -24,7 +24,6 @@ import {
|
|||
checkFetch,
|
||||
getSignatureUser,
|
||||
} from "@/remote/activitypub/check-fetch.js";
|
||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||
import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
||||
import Featured from "./activitypub/featured.js";
|
||||
import Following from "./activitypub/following.js";
|
||||
|
@ -296,7 +295,7 @@ router.get("/users/:user/collections/featured", Featured);
|
|||
|
||||
// publickey
|
||||
router.get("/users/:user/publickey", async (ctx) => {
|
||||
const instanceActor = await getInstanceActor();
|
||||
const instanceActor = await getInternalActor("instance");
|
||||
if (ctx.params.user === instanceActor.id) {
|
||||
ctx.body = renderActivity(
|
||||
renderKey(instanceActor, await getUserKeypair(instanceActor.id)),
|
||||
|
@ -360,7 +359,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
|
|||
router.get("/users/:user", async (ctx, next) => {
|
||||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
const instanceActor = await getInstanceActor();
|
||||
const instanceActor = await getInternalActor("instance");
|
||||
if (ctx.params.user === instanceActor.id) {
|
||||
await userInfo(ctx, instanceActor);
|
||||
return;
|
||||
|
@ -387,7 +386,7 @@ router.get("/@:user", async (ctx, next) => {
|
|||
if (!isActivityPubReq(ctx)) return await next();
|
||||
|
||||
if (ctx.params.user === "instance.actor") {
|
||||
const instanceActor = await getInstanceActor();
|
||||
const instanceActor = await getInternalActor("instance");
|
||||
await userInfo(ctx, instanceActor);
|
||||
return;
|
||||
}
|
||||
|
@ -407,8 +406,8 @@ router.get("/@:user", async (ctx, next) => {
|
|||
await userInfo(ctx, user);
|
||||
});
|
||||
|
||||
router.get("/actor", async (ctx, next) => {
|
||||
const instanceActor = await getInstanceActor();
|
||||
router.get("/actor", async (ctx, _next) => {
|
||||
const instanceActor = await getInternalActor("instance");
|
||||
await userInfo(ctx, instanceActor);
|
||||
});
|
||||
//#endregion
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { Brackets, type SelectQueryBuilder } from "typeorm";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import { Followings, Notes } from "@/models/index.js";
|
||||
import { Cache } from "@/misc/cache.js";
|
||||
import { apiLogger } from "@/server/api/logger.js";
|
||||
|
||||
const cache = new Cache<number>("homeTlQueryData", 60 * 60 * 24);
|
||||
const cutoff = 250; // 250 posts in the last 7 days, constant determined by comparing benchmarks for cutoff values between 100 and 2500
|
||||
const logger = apiLogger.createSubLogger("heuristics");
|
||||
|
||||
export async function generateFollowingQuery(
|
||||
q: SelectQueryBuilder<any>,
|
||||
me: { id: User["id"] },
|
||||
): Promise<void> {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :meId");
|
||||
|
||||
const heuristic = await cache.fetch(me.id, async () => {
|
||||
const curr = new Date();
|
||||
const prev = new Date();
|
||||
prev.setDate(prev.getDate() - 7);
|
||||
return Notes.createQueryBuilder("note")
|
||||
.where("note.createdAt > :prev", { prev })
|
||||
.andWhere("note.createdAt < :curr", { curr })
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()})`);
|
||||
qb.orWhere("note.userId = :meId", { meId: me.id });
|
||||
}),
|
||||
)
|
||||
.getCount()
|
||||
.then((res) => {
|
||||
logger.info(
|
||||
`Calculating heuristics for user ${me.id} took ${
|
||||
new Date().getTime() - curr.getTime()
|
||||
}ms`,
|
||||
);
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
const shouldUseUnion = heuristic < cutoff;
|
||||
|
||||
q.andWhere(
|
||||
new Brackets((qb) => {
|
||||
if (shouldUseUnion) {
|
||||
qb.where(
|
||||
`note.userId = ANY(array(${followingQuery.getQuery()} UNION ALL VALUES (:meId)))`,
|
||||
);
|
||||
} else {
|
||||
qb.where("note.userId = :meId");
|
||||
qb.orWhere(`note.userId IN (${followingQuery.getQuery()})`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
q.setParameters({ meId: me.id });
|
||||
}
|
430
packages/backend/src/server/api/common/generate-fts-query.ts
Normal file
430
packages/backend/src/server/api/common/generate-fts-query.ts
Normal file
|
@ -0,0 +1,430 @@
|
|||
import {
|
||||
Brackets,
|
||||
type SelectQueryBuilder,
|
||||
type WhereExpressionBuilder,
|
||||
} from "typeorm";
|
||||
import { sqlLikeEscape, sqlRegexEscape } from "backend-rs";
|
||||
import {
|
||||
Followings,
|
||||
NoteFavorites,
|
||||
NoteReactions,
|
||||
Users,
|
||||
} from "@/models/index.js";
|
||||
import type { Note } from "@/models/entities/note";
|
||||
import type { Following } from "@/models/entities/following";
|
||||
import type { NoteFavorite } from "@/models/entities/note-favorite";
|
||||
|
||||
const filters = {
|
||||
from: fromFilter,
|
||||
"-from": fromFilterInverse,
|
||||
mention: mentionFilter,
|
||||
"-mention": mentionFilterInverse,
|
||||
reply: replyFilter,
|
||||
"-reply": replyFilterInverse,
|
||||
to: replyFilter,
|
||||
"-to": replyFilterInverse,
|
||||
before: beforeFilter,
|
||||
until: beforeFilter,
|
||||
after: afterFilter,
|
||||
since: afterFilter,
|
||||
instance: instanceFilter,
|
||||
"-instance": instanceFilterInverse,
|
||||
domain: instanceFilter,
|
||||
"-domain": instanceFilterInverse,
|
||||
host: instanceFilter,
|
||||
"-host": instanceFilterInverse,
|
||||
filter: miscFilter,
|
||||
"-filter": miscFilterInverse,
|
||||
in: inFilter,
|
||||
"-in": inFilterInverse,
|
||||
has: attachmentFilter,
|
||||
} as Record<
|
||||
string,
|
||||
(query: SelectQueryBuilder<Note>, search: string, id: number) => void
|
||||
>;
|
||||
|
||||
export function generateFtsQuery(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
q: string,
|
||||
): void {
|
||||
const components = q.trim().split(" ");
|
||||
const terms: string[] = [];
|
||||
const finalTerms: string[] = [];
|
||||
let counter = 0;
|
||||
let caseSensitive = false;
|
||||
let matchWords = false;
|
||||
|
||||
for (const component of components) {
|
||||
const split = component.split(":");
|
||||
if (split.length > 1 && filters[split[0]] !== undefined)
|
||||
filters[split[0]](query, split.slice(1).join(":"), counter++);
|
||||
else if (
|
||||
split.length > 1 &&
|
||||
(split[0] === "search" || split[0] === "match")
|
||||
)
|
||||
matchWords = split[1] === "word" || split[1] === "words";
|
||||
else if (split.length > 1 && split[0] === "case")
|
||||
caseSensitive = split[1] === "sensitive";
|
||||
else terms.push(component);
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
let state: "idle" | "quote" | "parenthesis" = "idle";
|
||||
for (let i = 0; i < terms.length; i++) {
|
||||
if (state === "idle") {
|
||||
if (
|
||||
(terms[i].startsWith('"') && terms[i].endsWith('"')) ||
|
||||
(terms[i].startsWith("(") && terms[i].endsWith(")"))
|
||||
) {
|
||||
finalTerms.push(trimStartAndEnd(terms[i]));
|
||||
} else if (terms[i].startsWith('"')) {
|
||||
idx = i;
|
||||
state = "quote";
|
||||
} else if (terms[i].startsWith("(")) {
|
||||
idx = i;
|
||||
state = "parenthesis";
|
||||
} else {
|
||||
finalTerms.push(terms[i]);
|
||||
}
|
||||
} else if (state === "quote" && terms[i].endsWith('"')) {
|
||||
finalTerms.push(extractToken(terms, idx, i));
|
||||
state = "idle";
|
||||
} else if (state === "parenthesis" && terms[i].endsWith(")")) {
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
for (const term of extractToken(terms, idx, i).split(" OR ")) {
|
||||
const id = counter++;
|
||||
appendSearchQuery(
|
||||
term,
|
||||
"or",
|
||||
query,
|
||||
qb,
|
||||
id,
|
||||
term.startsWith("-"),
|
||||
matchWords,
|
||||
caseSensitive,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
state = "idle";
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== "idle") {
|
||||
finalTerms.push(
|
||||
...extractToken(terms, idx, terms.length - 1, false)
|
||||
.substring(1)
|
||||
.split(" "),
|
||||
);
|
||||
}
|
||||
|
||||
for (const term of finalTerms) {
|
||||
const id = counter++;
|
||||
appendSearchQuery(
|
||||
term,
|
||||
"and",
|
||||
query,
|
||||
query,
|
||||
id,
|
||||
term.startsWith("-"),
|
||||
matchWords,
|
||||
caseSensitive,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function fromFilter(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.userId = (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
|
||||
function fromFilterInverse(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.userId <> (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
|
||||
function mentionFilter(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.addCommonTableExpression(userQuery.getQuery(), `cte_${id}`, {
|
||||
materialized: true,
|
||||
});
|
||||
query.andWhere(
|
||||
`note.mentions @> array[(SELECT * FROM cte_${id})]::varchar[]`,
|
||||
);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
|
||||
function mentionFilterInverse(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.addCommonTableExpression(userQuery.getQuery(), `cte_${id}`, {
|
||||
materialized: true,
|
||||
});
|
||||
query.andWhere(
|
||||
`NOT (note.mentions @> array[(SELECT * FROM cte_${id})]::varchar[])`,
|
||||
);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
|
||||
function replyFilter(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.replyUserId = (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
|
||||
function replyFilterInverse(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
const userQuery = generateUserSubquery(filter, id);
|
||||
query.andWhere(`note.replyUserId <> (${userQuery.getQuery()})`);
|
||||
query.setParameters(userQuery.getParameters());
|
||||
}
|
||||
|
||||
function beforeFilter(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
query.andWhere("note.createdAt < :before", { before: filter });
|
||||
}
|
||||
|
||||
function afterFilter(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
query.andWhere("note.createdAt > :after", { after: filter });
|
||||
}
|
||||
|
||||
function instanceFilter(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
if (filter === "local") {
|
||||
query.andWhere("note.userHost IS NULL");
|
||||
} else {
|
||||
query.andWhere(`note.userHost = :instance_${id}`);
|
||||
query.setParameter(`instance_${id}`, filter);
|
||||
}
|
||||
}
|
||||
|
||||
function instanceFilterInverse(
|
||||
query: SelectQueryBuilder<Note>,
|
||||
filter: string,
|
||||
id: number,
|
||||
) {
|
||||
if (filter === "local") {
|
||||
query.andWhere("note.userHost IS NOT NULL");
|
||||
} else {
|
||||
query.andWhere(`note.userHost <> :instance_${id}`);
|
||||
query.setParameter(`instance_${id}`, filter);
|
||||
}
|
||||
}
|
||||
|
||||
function miscFilter(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
let subQuery: SelectQueryBuilder<Following> | null = null;
|
||||
if (filter === "followers") {
|
||||
subQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followerId")
|
||||
.where("following.followeeId = :meId");
|
||||
} else if (filter === "following") {
|
||||
subQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :meId");
|
||||
} else if (filter === "replies" || filter === "reply") {
|
||||
query.andWhere("note.replyId IS NOT NULL");
|
||||
} else if (
|
||||
filter === "boosts" ||
|
||||
filter === "boost" ||
|
||||
filter === "renotes" ||
|
||||
filter === "renote"
|
||||
) {
|
||||
query.andWhere("note.renoteId IS NOT NULL");
|
||||
}
|
||||
|
||||
if (subQuery !== null)
|
||||
query.andWhere(`note.userId IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
|
||||
function miscFilterInverse(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
let subQuery: SelectQueryBuilder<Following> | null = null;
|
||||
if (filter === "followers") {
|
||||
subQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followerId")
|
||||
.where("following.followeeId = :meId");
|
||||
} else if (filter === "following") {
|
||||
subQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :meId");
|
||||
} else if (filter === "replies" || filter === "reply") {
|
||||
query.andWhere("note.replyId IS NULL");
|
||||
} else if (
|
||||
filter === "boosts" ||
|
||||
filter === "boost" ||
|
||||
filter === "renotes" ||
|
||||
filter === "renote"
|
||||
) {
|
||||
query.andWhere("note.renoteId IS NULL");
|
||||
}
|
||||
|
||||
if (subQuery !== null)
|
||||
query.andWhere(`note.userId NOT IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
|
||||
function inFilter(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
let subQuery: SelectQueryBuilder<NoteFavorite> | null = null;
|
||||
if (filter === "bookmarks") {
|
||||
subQuery = NoteFavorites.createQueryBuilder("bookmark")
|
||||
.select("bookmark.noteId")
|
||||
.where("bookmark.userId = :meId");
|
||||
} else if (
|
||||
filter === "favorites" ||
|
||||
filter === "favourites" ||
|
||||
filter === "reactions" ||
|
||||
filter === "likes"
|
||||
) {
|
||||
subQuery = NoteReactions.createQueryBuilder("react")
|
||||
.select("react.noteId")
|
||||
.where("react.userId = :meId");
|
||||
}
|
||||
|
||||
if (subQuery !== null) query.andWhere(`note.id IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
|
||||
function inFilterInverse(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
let subQuery: SelectQueryBuilder<NoteFavorite> | null = null;
|
||||
if (filter === "bookmarks") {
|
||||
subQuery = NoteFavorites.createQueryBuilder("bookmark")
|
||||
.select("bookmark.noteId")
|
||||
.where("bookmark.userId = :meId");
|
||||
} else if (
|
||||
filter === "favorites" ||
|
||||
filter === "favourites" ||
|
||||
filter === "reactions" ||
|
||||
filter === "likes"
|
||||
) {
|
||||
subQuery = NoteReactions.createQueryBuilder("react")
|
||||
.select("react.noteId")
|
||||
.where("react.userId = :meId");
|
||||
}
|
||||
|
||||
if (subQuery !== null)
|
||||
query.andWhere(`note.id NOT IN (${subQuery.getQuery()})`);
|
||||
}
|
||||
|
||||
function attachmentFilter(query: SelectQueryBuilder<Note>, filter: string) {
|
||||
switch (filter) {
|
||||
case "image":
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%image/%'`);
|
||||
break;
|
||||
case "video":
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%video/%'`);
|
||||
break;
|
||||
case "audio":
|
||||
query.andWhere(`note."attachedFileTypes"::varchar ILIKE '%audio/%'`);
|
||||
break;
|
||||
case "file":
|
||||
query.andWhere(`note."attachedFileTypes" <> '{}'`);
|
||||
query.andWhere(
|
||||
`NOT (note."attachedFileTypes"::varchar ILIKE '%image/%')`,
|
||||
);
|
||||
query.andWhere(
|
||||
`NOT (note."attachedFileTypes"::varchar ILIKE '%video/%')`,
|
||||
);
|
||||
query.andWhere(
|
||||
`NOT (note."attachedFileTypes"::varchar ILIKE '%audio/%')`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function generateUserSubquery(filter: string, id: number) {
|
||||
if (filter.startsWith("@")) filter = filter.substring(1);
|
||||
const split = filter.split("@");
|
||||
|
||||
const query = Users.createQueryBuilder("user")
|
||||
.select("user.id")
|
||||
.where(`user.usernameLower = :user_${id}`)
|
||||
.andWhere(
|
||||
`user.host ${split[1] !== undefined ? `= :host_${id}` : "IS NULL"}`,
|
||||
);
|
||||
|
||||
query.setParameter(`user_${id}`, split[0].toLowerCase());
|
||||
|
||||
if (split[1] !== undefined)
|
||||
query.setParameter(`host_${id}`, split[1].toLowerCase());
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function extractToken(
|
||||
array: string[],
|
||||
start: number,
|
||||
end: number,
|
||||
trim = true,
|
||||
) {
|
||||
const slice = array.slice(start, end + 1).join(" ");
|
||||
return trim ? trimStartAndEnd(slice) : slice;
|
||||
}
|
||||
|
||||
function trimStartAndEnd(str: string) {
|
||||
return str.substring(1, str.length - 1);
|
||||
}
|
||||
|
||||
function appendSearchQuery(
|
||||
term: string,
|
||||
mode: "and" | "or",
|
||||
query: SelectQueryBuilder<Note>,
|
||||
qb: SelectQueryBuilder<Note> | WhereExpressionBuilder,
|
||||
id: number,
|
||||
negate: boolean,
|
||||
matchWords: boolean,
|
||||
caseSensitive: boolean,
|
||||
) {
|
||||
const sql = `note.text ${getSearchMatchOperator(
|
||||
negate,
|
||||
matchWords,
|
||||
caseSensitive,
|
||||
)} :q_${id}`;
|
||||
if (mode === "and") qb.andWhere(sql);
|
||||
else if (mode === "or") qb.orWhere(sql);
|
||||
query.setParameter(
|
||||
`q_${id}`,
|
||||
escapeSqlSearchParam(term.substring(negate ? 1 : 0), matchWords),
|
||||
);
|
||||
}
|
||||
|
||||
function getSearchMatchOperator(
|
||||
negate: boolean,
|
||||
matchWords: boolean,
|
||||
caseSensitive: boolean,
|
||||
) {
|
||||
return `${negate ? "NOT " : ""}${
|
||||
matchWords ? (caseSensitive ? "~" : "~*") : caseSensitive ? "LIKE" : "ILIKE"
|
||||
}`;
|
||||
}
|
||||
|
||||
function escapeSqlSearchParam(param: string, matchWords: boolean) {
|
||||
return matchWords
|
||||
? `\\y${sqlRegexEscape(param)}\\y`
|
||||
: `%${sqlLikeEscape(param)}%`;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Brackets, type SelectQueryBuilder } from "typeorm";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import { UserListJoinings, UserLists } from "@/models/index.js";
|
||||
|
||||
export function generateListQuery(
|
||||
q: SelectQueryBuilder<any>,
|
||||
me: { id: User["id"] },
|
||||
): void {
|
||||
const listQuery = UserLists.createQueryBuilder("list")
|
||||
.select("list.id")
|
||||
.andWhere("list.userId = :meId");
|
||||
|
||||
const memberQuery = UserListJoinings.createQueryBuilder("member")
|
||||
.select("member.userId")
|
||||
.where(`member.userListId IN (${listQuery.getQuery()})`);
|
||||
|
||||
q.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where("note.userId = :meId");
|
||||
qb.orWhere(`note.userId NOT IN (${memberQuery.getQuery()})`);
|
||||
}),
|
||||
);
|
||||
|
||||
q.setParameters({ meId: me.id });
|
||||
}
|
|
@ -3,7 +3,13 @@ import { User } from "@/models/entities/user.js";
|
|||
import { Users, UsedUsernames } from "@/models/index.js";
|
||||
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { genIdAt, generateUserToken, hashPassword, toPuny } from "backend-rs";
|
||||
import {
|
||||
countLocalUsers,
|
||||
genIdAt,
|
||||
generateUserToken,
|
||||
hashPassword,
|
||||
toPuny,
|
||||
} from "backend-rs";
|
||||
import { UserKeypair } from "@/models/entities/user-keypair.js";
|
||||
import { UsedUsername } from "@/models/entities/used-username.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
|
@ -18,9 +24,7 @@ export async function signup(opts: {
|
|||
const { username, password, passwordHash, host } = opts;
|
||||
let hash = passwordHash;
|
||||
|
||||
const userCount = await Users.countBy({
|
||||
host: IsNull(),
|
||||
});
|
||||
const userCount = await countLocalUsers();
|
||||
|
||||
if (config.maxUserSignups != null && userCount > config.maxUserSignups) {
|
||||
throw new Error("MAX_USERS_REACHED");
|
||||
|
@ -103,11 +107,7 @@ export async function signup(opts: {
|
|||
usernameLower: username.toLowerCase(),
|
||||
host: host == null ? null : toPuny(host),
|
||||
token: secret,
|
||||
isAdmin:
|
||||
(await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
})) === 0,
|
||||
isAdmin: (await countLocalUsers()) === 0,
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import define from "@/server/api/define.js";
|
||||
import { Users } from "@/models/index.js";
|
||||
import { signup } from "@/server/api/common/signup.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { countLocalUsers } from "backend-rs";
|
||||
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
@ -32,11 +32,8 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps, _me, token) => {
|
||||
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
|
||||
const noUsers =
|
||||
(await Users.countBy({
|
||||
host: IsNull(),
|
||||
})) === 0;
|
||||
if (!noUsers && !me?.isAdmin) throw new Error("access denied");
|
||||
if (!me?.isAdmin && (await countLocalUsers()) !== 0)
|
||||
throw new Error("access denied");
|
||||
if (token) throw new Error("access denied");
|
||||
|
||||
const { account, secret } = await signup({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import define from "@/server/api/define.js";
|
||||
import { AbuseUserReports, Users } from "@/models/index.js";
|
||||
import { getInstanceActor } from "@/services/instance-actor.js";
|
||||
import { getInternalActor } from "backend-rs";
|
||||
import { deliver } from "@/queue/index.js";
|
||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||
import { renderFlag } from "@/remote/activitypub/renderer/flag.js";
|
||||
|
@ -29,7 +29,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
|
||||
if (ps.forward && report.targetUserHost != null) {
|
||||
const actor = await getInstanceActor();
|
||||
const actor = await getInternalActor("instance");
|
||||
const targetUser = await Users.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
deliver(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import JSON5 from "json5";
|
||||
import { IsNull, MoreThan } from "typeorm";
|
||||
import { config } from "@/config.js";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
import { countLocalUsers, fetchMeta } from "backend-rs";
|
||||
import { Ads, Emojis, Users } from "@/models/index.js";
|
||||
import define from "@/server/api/define.js";
|
||||
|
||||
|
@ -501,11 +501,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
instance.privateMode && !me ? [] : instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
markLocalFilesNsfwByDefault: instance.markLocalFilesNsfwByDefault,
|
||||
requireSetup:
|
||||
(await Users.countBy({
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
})) === 0,
|
||||
requireSetup: (await countLocalUsers()) === 0,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import { Notes } from "@/models/index.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
|
||||
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import { sqlLikeEscape } from "backend-rs";
|
||||
import type { SelectQueryBuilder } from "typeorm";
|
||||
import { searchNotes, type SearchParams } from "@/misc/search.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["notes"],
|
||||
|
@ -69,76 +65,23 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
async function search(
|
||||
modifier?: (query: SelectQueryBuilder<Note>) => void,
|
||||
): Promise<Note[]> {
|
||||
const query = makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
ps.sinceId,
|
||||
ps.untilId,
|
||||
ps.sinceDate ?? undefined,
|
||||
ps.untilDate ?? undefined,
|
||||
);
|
||||
modifier?.(query);
|
||||
|
||||
if (ps.userId != null) {
|
||||
query.andWhere("note.userId = :userId", { userId: ps.userId });
|
||||
}
|
||||
|
||||
if (ps.channelId != null) {
|
||||
query.andWhere("note.channelId = :channelId", {
|
||||
channelId: ps.channelId,
|
||||
});
|
||||
}
|
||||
|
||||
query.innerJoinAndSelect("note.user", "user");
|
||||
|
||||
// "from: me": search all (public, home, followers, specified) my posts
|
||||
// otherwise: search public indexable posts only
|
||||
if (ps.userId == null || ps.userId !== me?.id) {
|
||||
query
|
||||
.andWhere("note.visibility = 'public'")
|
||||
.andWhere("user.isIndexable = TRUE");
|
||||
}
|
||||
|
||||
if (ps.userId != null) {
|
||||
query.andWhere("note.userId = :userId", { userId: ps.userId });
|
||||
}
|
||||
|
||||
if (ps.host === null) {
|
||||
query.andWhere("note.userHost IS NULL");
|
||||
}
|
||||
if (ps.host != null) {
|
||||
query.andWhere("note.userHost = :userHost", { userHost: ps.host });
|
||||
}
|
||||
|
||||
if (ps.withFiles === true) {
|
||||
query.andWhere("note.fileIds != '{}'");
|
||||
}
|
||||
|
||||
query
|
||||
.leftJoinAndSelect("user.avatar", "avatar")
|
||||
.leftJoinAndSelect("user.banner", "banner")
|
||||
.leftJoinAndSelect("note.reply", "reply")
|
||||
.leftJoinAndSelect("note.renote", "renote")
|
||||
.leftJoinAndSelect("reply.user", "replyUser")
|
||||
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
|
||||
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
|
||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
||||
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
|
||||
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
|
||||
|
||||
generateVisibilityQuery(query, me);
|
||||
if (me) generateMutedUserQuery(query, me);
|
||||
if (me) generateBlockedUserQuery(query, me);
|
||||
|
||||
return await query.take(ps.limit).getMany();
|
||||
}
|
||||
|
||||
let notes: Note[];
|
||||
|
||||
const params: SearchParams = {
|
||||
withFilesOnly: ps.withFiles ?? false,
|
||||
limit: ps.limit,
|
||||
myId: me?.id,
|
||||
sinceId: ps.sinceId,
|
||||
untilId: ps.untilId,
|
||||
sinceDate: ps.sinceDate ?? undefined,
|
||||
untilDate: ps.untilDate ?? undefined,
|
||||
userId: ps.userId,
|
||||
channelId: ps.channelId,
|
||||
host: ps.host,
|
||||
};
|
||||
|
||||
if (ps.query != null) {
|
||||
const q = sqlLikeEscape(ps.query);
|
||||
const searchWord = sqlLikeEscape(ps.query);
|
||||
|
||||
if (ps.searchCwAndAlt) {
|
||||
// Whether we should return latest notes first
|
||||
|
@ -159,15 +102,15 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
...new Map(
|
||||
(
|
||||
await Promise.all([
|
||||
search((query) => {
|
||||
query.andWhere("note.text &@~ :q", { q });
|
||||
searchNotes(params, (query) => {
|
||||
query.andWhere("note.text &@~ :q", { q: searchWord });
|
||||
}),
|
||||
search((query) => {
|
||||
query.andWhere("note.cw &@~ :q", { q });
|
||||
searchNotes(params, (query) => {
|
||||
query.andWhere("note.cw &@~ :q", { q: searchWord });
|
||||
}),
|
||||
search((query) => {
|
||||
searchNotes(params, (query) => {
|
||||
query
|
||||
.andWhere("drive_file.comment &@~ :q", { q })
|
||||
.andWhere("drive_file.comment &@~ :q", { q: searchWord })
|
||||
.innerJoin("note.files", "drive_file");
|
||||
}),
|
||||
])
|
||||
|
@ -179,12 +122,12 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
.sort(compare)
|
||||
.slice(0, ps.limit);
|
||||
} else {
|
||||
notes = await search((query) => {
|
||||
query.andWhere("note.text &@~ :q", { q });
|
||||
notes = await searchNotes(params, (query) => {
|
||||
query.andWhere("note.text &@~ :q", { q: searchWord });
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notes = await search();
|
||||
notes = await searchNotes(params);
|
||||
}
|
||||
|
||||
return await Notes.packMany(notes, me);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Instances, Users, Notes } from "@/models/index.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { countLocalUsers } from "backend-rs";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
@ -69,11 +70,7 @@ export default define(meta, paramDef, async () => {
|
|||
// usersCount
|
||||
Users.count(),
|
||||
// originalUsersCount
|
||||
Users.count({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
},
|
||||
}),
|
||||
countLocalUsers(),
|
||||
// instances
|
||||
Instances.count(),
|
||||
]);
|
||||
|
|
|
@ -7,10 +7,7 @@ import Router from "@koa/router";
|
|||
import multer from "@koa/multer";
|
||||
import bodyParser from "koa-bodyparser";
|
||||
import cors from "@koa/cors";
|
||||
import {
|
||||
apiMastodonCompatible,
|
||||
getClient,
|
||||
} from "./mastodon/ApiMastodonCompatibleService.js";
|
||||
import { setupMastodonApi } from "./mastodon/index.js";
|
||||
import { AccessTokens, Users } from "@/models/index.js";
|
||||
import { config } from "@/config.js";
|
||||
import endpoints from "./endpoints.js";
|
||||
|
@ -20,10 +17,6 @@ import signup from "./private/signup.js";
|
|||
import signin from "./private/signin.js";
|
||||
import signupPending from "./private/signup-pending.js";
|
||||
import verifyEmail from "./private/verify-email.js";
|
||||
import { koaBody } from "koa-body";
|
||||
import { convertAttachment } from "./mastodon/converters.js";
|
||||
import { apiLogger } from "./logger.js";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
// Init app
|
||||
const app = new Koa();
|
||||
|
@ -66,64 +59,7 @@ router.use(
|
|||
}),
|
||||
);
|
||||
|
||||
mastoRouter.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
urlencoded: true,
|
||||
}),
|
||||
);
|
||||
|
||||
mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const multipartData = await ctx.file;
|
||||
if (!multipartData) {
|
||||
ctx.body = { error: "No image" };
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
const data = await client.uploadMedia(multipartData);
|
||||
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const multipartData = await ctx.file;
|
||||
if (!multipartData) {
|
||||
ctx.body = { error: "No image" };
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
const data = await client.uploadMedia(multipartData, ctx.request.body);
|
||||
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
mastoRouter.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
apiMastodonCompatible(mastoRouter);
|
||||
setupMastodonApi(mastoRouter);
|
||||
|
||||
/**
|
||||
* Register endpoint handlers
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
import Router from "@koa/router";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import { apiAuthMastodon } from "./endpoints/auth.js";
|
||||
import { apiAccountMastodon } from "./endpoints/account.js";
|
||||
import { apiStatusMastodon } from "./endpoints/status.js";
|
||||
import { apiFilterMastodon } from "./endpoints/filter.js";
|
||||
import { apiTimelineMastodon } from "./endpoints/timeline.js";
|
||||
import { apiNotificationsMastodon } from "./endpoints/notifications.js";
|
||||
import { apiSearchMastodon } from "./endpoints/search.js";
|
||||
import { getInstance } from "./endpoints/meta.js";
|
||||
import {
|
||||
convertAccount,
|
||||
convertAnnouncement,
|
||||
convertFilter,
|
||||
} from "./converters.js";
|
||||
import { fromMastodonId } from "backend-rs";
|
||||
import { Users } from "@/models/index.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { apiLogger } from "../logger.js";
|
||||
import { inspect } from "node:util";
|
||||
|
||||
export function getClient(
|
||||
BASE_URL: string,
|
||||
authorization: string | undefined,
|
||||
): MegalodonInterface {
|
||||
const accessTokenArr = authorization?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator(BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
}
|
||||
|
||||
export function apiMastodonCompatible(router: Router): void {
|
||||
apiAuthMastodon(router);
|
||||
apiAccountMastodon(router);
|
||||
apiStatusMastodon(router);
|
||||
apiFilterMastodon(router);
|
||||
apiTimelineMastodon(router);
|
||||
apiNotificationsMastodon(router);
|
||||
apiSearchMastodon(router);
|
||||
|
||||
router.get("/v1/custom_emojis", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceCustomEmojis();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/instance", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstance();
|
||||
const admin = await Users.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
},
|
||||
order: { id: "ASC" },
|
||||
});
|
||||
const contact =
|
||||
admin == null
|
||||
? null
|
||||
: convertAccount((await client.getAccount(admin.id)).data);
|
||||
ctx.body = await getInstance(data.data, contact);
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/announcements", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceAnnouncements();
|
||||
ctx.body = data.data.map((announcement) =>
|
||||
convertAnnouncement(announcement),
|
||||
);
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/announcements/:id/dismiss",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.dismissInstanceAnnouncement(
|
||||
fromMastodonId(ctx.params.id),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
ctx.body = data.data.map((filter) => convertFilter(filter));
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/trends", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstanceTrends();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/preferences", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getPreferences();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
apiLogger.error(inspect(e));
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import type { Entity } from "megalodon";
|
||||
import { toMastodonId } from "backend-rs";
|
||||
|
||||
function simpleConvert(data: any) {
|
||||
// copy the object to bypass weird pass by reference bugs
|
||||
const result = Object.assign({}, data);
|
||||
result.id = toMastodonId(data.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertAccount(account: Entity.Account) {
|
||||
return simpleConvert(account);
|
||||
}
|
||||
export function convertAnnouncement(announcement: Entity.Announcement) {
|
||||
return simpleConvert(announcement);
|
||||
}
|
||||
export function convertAttachment(attachment: Entity.Attachment) {
|
||||
const converted = simpleConvert(attachment);
|
||||
// ref: https://github.com/whitescent/Mastify/pull/102
|
||||
if (converted.meta == null) return converted;
|
||||
const result = {
|
||||
...converted,
|
||||
meta: {
|
||||
...converted.meta,
|
||||
original: {
|
||||
...converted.meta,
|
||||
},
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}
|
||||
export function convertFilter(filter: Entity.Filter) {
|
||||
return simpleConvert(filter);
|
||||
}
|
||||
export function convertList(list: Entity.List) {
|
||||
return simpleConvert(list);
|
||||
}
|
||||
export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
||||
return simpleConvert(tag);
|
||||
}
|
||||
|
||||
export function convertNotification(notification: Entity.Notification) {
|
||||
notification.account = convertAccount(notification.account);
|
||||
notification.id = toMastodonId(notification.id);
|
||||
if (notification.status)
|
||||
notification.status = convertStatus(notification.status);
|
||||
if (notification.reaction)
|
||||
notification.reaction = convertReaction(notification.reaction);
|
||||
return notification;
|
||||
}
|
||||
|
||||
export function convertPoll(poll: Entity.Poll) {
|
||||
return simpleConvert(poll);
|
||||
}
|
||||
export function convertReaction(reaction: Entity.Reaction) {
|
||||
if (reaction.accounts) {
|
||||
reaction.accounts = reaction.accounts.map(convertAccount);
|
||||
}
|
||||
return reaction;
|
||||
}
|
||||
export function convertRelationship(relationship: Entity.Relationship) {
|
||||
return simpleConvert(relationship);
|
||||
}
|
||||
|
||||
export function convertStatus(status: Entity.Status) {
|
||||
status.account = convertAccount(status.account);
|
||||
status.id = toMastodonId(status.id);
|
||||
if (status.in_reply_to_account_id)
|
||||
status.in_reply_to_account_id = toMastodonId(status.in_reply_to_account_id);
|
||||
if (status.in_reply_to_id)
|
||||
status.in_reply_to_id = toMastodonId(status.in_reply_to_id);
|
||||
status.media_attachments = status.media_attachments.map((attachment) =>
|
||||
convertAttachment(attachment),
|
||||
);
|
||||
status.mentions = status.mentions.map((mention) => ({
|
||||
...mention,
|
||||
id: toMastodonId(mention.id),
|
||||
}));
|
||||
if (status.poll) status.poll = convertPoll(status.poll);
|
||||
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
||||
if (status.quote) status.quote = convertStatus(status.quote);
|
||||
status.reactions = status.reactions.map(convertReaction);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
export function convertConversation(conversation: Entity.Conversation) {
|
||||
conversation.id = toMastodonId(conversation.id);
|
||||
conversation.accounts = conversation.accounts.map(convertAccount);
|
||||
if (conversation.last_status) {
|
||||
conversation.last_status = convertStatus(conversation.last_status);
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue