Merge branch 'develop' into feat/scylladb

This commit is contained in:
Namekuji 2023-09-04 19:30:22 -04:00
commit c2b3e81936
No known key found for this signature in database
GPG key ID: 1D62332C07FBA532
59 changed files with 1591 additions and 411 deletions

View file

@ -170,11 +170,11 @@ reservedUsernames: [
# Whether disable HSTS # Whether disable HSTS
#disableHsts: true #disableHsts: true
# Number of worker processes # Number of worker processes by type.
#clusterLimit: 1 # The sum must not exceed the number of available cores.
#clusterLimits:
# Worker only mode # web: 1
#onlyQueueProcessor: 1 # queue: 1
# Job concurrency per worker # Job concurrency per worker
# deliverJobConcurrency: 128 # deliverJobConcurrency: 128

View file

@ -3,7 +3,13 @@
**What does this PR do?** _(Please give us a brief description of what this PR does.)_ **What does this PR do?** _(Please give us a brief description of what this PR does.)_
**Contribution Guidelines** **Contribution Guidelines**
By submitting this issue, you agree to follow our [Contribution Guidelines](https://git.joinfirefish.org/firefish/firefish/-/blob/develop/CONTRIBUTING.md) By submitting this merge request, you agree to follow our [Contribution Guidelines](https://git.joinfirefish.org/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
- [ ] I agree to follow this project's Contribution Guidelines - [ ] I agree to follow this project's Contribution Guidelines
- [ ] I have made sure to test this pull request - [ ] I have made sure to test this pull request
- [ ] I have made sure to run `pnpm run format` before submitting this pull request - [ ] I have made sure to run `pnpm run format` before submitting this pull request
If this merge request makes changes to the Firefish API, please update `docs/api-change.md`
- [ ] I updated the documentation
<!-- Uncomment if your merge request has multiple authors -->
<!-- Co-authored-by: Name <email@email.com> -->

View file

@ -1,6 +1,6 @@
# Changelog # Changelog
## [1.0.4-beta] - 2023-08-02 ## [1.0.4-beta2] - 2023-09-02
### Bug Fixes ### Bug Fixes
@ -16,8 +16,6 @@
- Fix: :bug: make admin users page properly direct user cards to about page - Fix: :bug: make admin users page properly direct user cards to about page
- Fix?
- Fix: :globe_with_meridians: copying origin: "remote" -> "origin" - Fix: :globe_with_meridians: copying origin: "remote" -> "origin"
- Fix: :lipstick: don't round corners on status images/server icon - Fix: :lipstick: don't round corners on status images/server icon
@ -106,23 +104,72 @@ Closes #10581
- Fix: :busts_in_silhouette: naskya is fullstack - Fix: :busts_in_silhouette: naskya is fullstack
- Fix Japanese locale - Fix: :green_heart: Docker env for CI
### Documentation - Fix: :green_heart: docker service alias
- Docs: :memo: repo move - Fix: :lipstick: properly style announcement content, add seperator
- Docs: :memo: fix link - Fix: generate stream id with timestamp
- Docs: :memo: 1.0.3 changelog - Fix: add original id to the stream
- Docs: :memo: Add explicit FoundKey commits to CHANGELOG - Fix: :bug: double-slash in proxy url
- Docs: :memo: AUR package - Fix: :bug: null host meilisearch
- Docs: :memo: remove links to FIREFISH.md - Fix: :globe_with_meridians: i18n key for "confirm"
Closes #10601 - Fix: :bug: offline html responsive viewport
- Fix: building re2 in Dockerfile
- Fix: :children_crossing: make importing emoji packs clearer
- Fix: updatePerson's Followings.update call not working if no sharedInbox
- Fix: :bug: double comma
- Fix: :adhesive_bandage: add small and center to MFM tags list
- Fix: veiry url
- Fix: exclude localhost
- Fix: exclude ula and lla
- Fix: remove brackets
- Fix: 🚑 thumbnail serving
- Fix: change character limits to allow for long instance domains
- Fix: :pencil2: "can not" -> "cannot"
- Fix: :lock: cannot change note visibility
- Fix: :bug: cannot quote own note
- Fix: :bug: fix DNS lookup issue
https://github.com/szmarczak/cacheable-lookup/pull/62
- Fix: :bug: caching wrong DNS entry when federating with an instance that cannot properly handle inbound IPv6 requests
- Fix: remove native-utils from migration's dependencies
- Fix: :recycle: Manifest (icons, name, orientation)
Closes #10694
- Fix: :bug: unlock undefined
- Fix: :bug: more strange unlock calls
- Fix: :bug: natural PWA orientation
fixes chrome mobile rotating screen even when device rotation is off
- Fix: :lipstick: Announcement padding/margins
### Features ### Features
@ -149,6 +196,12 @@ ref: https://frfsh.plus.st/notes/9hqswpwiwjaihcgo
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chiken.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chiken.com>
- Add migration to fix corrupted stream ids
- Feat: Fetch total posts of users on create/update
- Feat: post translation in Traditional Chinese
### Miscellaneous Tasks ### Miscellaneous Tasks
@ -758,11 +811,491 @@ Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Merge branch 'origin/develop' into Weblate. - Chore: Merge branch 'origin/develop' into Weblate.
- Chore: update ja-JP.yml - Chore: update ja-JP.yml
- Chore: :bookmark: v1.0.4-beta
- Chore: Translated using Weblate (French)
Currently translated at 99.7% (1839 of 1844 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (Japanese)
Currently translated at 100.0% (1844 of 1844 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja/
- Chore: Translated using Weblate (Turkish)
Currently translated at 99.9% (1843 of 1844 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/tr/
- Chore: Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1844 of 1844 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/uk/
- Chore: Translated using Weblate (Bulgarian (bul_BG))
Currently translated at 20.9% (386 of 1844 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/bul_BG/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (French)
Currently translated at 99.6% (1839 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (Catalan)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ca/
- Chore: Translated using Weblate (French)
Currently translated at 99.6% (1839 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (French)
Currently translated at 99.6% (1839 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Indonesian)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/id/
- Chore: Translated using Weblate (Portuguese (Brazil))
Currently translated at 12.0% (222 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/pt_BR/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (German)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Translated using Weblate (German)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Translated using Weblate (French)
Currently translated at 99.7% (1841 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (Norwegian Bokmål)
Currently translated at 32.4% (599 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/nb_NO/
- Chore: Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/uk/
- Chore: Translated using Weblate (Vietnamese)
Currently translated at 93.1% (1720 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/vi/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: up pnpm to 8.6.11
- Chore: :arrow_up: up deps
- Chore: :art: format
- Chore: Translated using Weblate (Norwegian Bokmål)
Currently translated at 36.1% (668 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/nb_NO/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: remove unused items
- Chore: :art: format
- Chore: Translated using Weblate (Spanish)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/es/
- Chore: Translated using Weblate (French)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (French)
Currently translated at 100.0% (1846 of 1846 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Catalan)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ca/
- Chore: Translated using Weblate (French)
Currently translated at 99.5% (1842 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (Indonesian)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/id/
- Chore: Translated using Weblate (Japanese)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja/
- Chore: Translated using Weblate (Russian)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ru/
- Chore: Translated using Weblate (Bulgarian (bul_BG))
Currently translated at 23.4% (433 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/bul_BG/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: codeberg → gitlab
- Chore: Translated using Weblate (German)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Japanese)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja/
- Chore: Translated using Weblate (German)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/uk/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Japanese)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja/
- Chore: Translated using Weblate (Japanese (Kansai))
Currently translated at 69.1% (1280 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja_KS/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Catalan)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ca/
- Chore: Translated using Weblate (German)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Translated using Weblate (German)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Translated using Weblate (French)
Currently translated at 99.6% (1844 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (Indonesian)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/id/
- Chore: Translated using Weblate (Japanese)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja/
- Chore: Translated using Weblate (Japanese (Kansai))
Currently translated at 69.9% (1294 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja_KS/
- Chore: Translated using Weblate (Norwegian Bokmål)
Currently translated at 54.0% (1000 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/nb_NO/
- Chore: Translated using Weblate (Bulgarian (bul_BG))
Currently translated at 23.5% (435 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/bul_BG/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Italian)
Currently translated at 88.4% (1637 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/it/
- Chore: Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/zh_Hans/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: :art: format
- Chore: Translated using Weblate (Italian)
Currently translated at 98.5% (1824 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/it/
- Chore: Translated using Weblate (Russian)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ru/
- Chore: Translated using Weblate (Korean)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ko/
- Chore: Merge branch 'origin/develop' into Weblate.
- Update stop words
- Chore: :art: format
- Chore: Translated using Weblate (Indonesian)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/id/
- Chore: Translated using Weblate (Italian)
Currently translated at 98.9% (1830 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/it/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (French)
Currently translated at 99.6% (1843 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
- Chore: Translated using Weblate (Italian)
Currently translated at 98.9% (1831 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/it/
- Chore: Translated using Weblate (Korean)
Currently translated at 100.0% (1850 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ko/
- Chore: Translated using Weblate (Thai)
Currently translated at 56.5% (1047 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/th/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: :technologist: More recommended VSCode extensions
Iconify to preview Phosphor icons, Conventional Commits for commit style
- Chore: :technologist: More recommended VSCode extensions
Docker, GitLab Workflow, JSON5, Prettier, YAML, and Pretty TS Errors
- Chore: Translated using Weblate (Thai)
Currently translated at 58.6% (1085 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/th/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore, refactor: remove unused, fix some type errors (client/src/pages)
- Chore: :rotating_light: lint
- Chore: :wrench: linting config
- Chore: 🚨 lint megalodon
- Chore: :arrow_up: up deps (properly)
- Chore: :hammer: build greet js -> sh
- Chore: :art: script format
- Chore: Translated using Weblate (Italian)
Currently translated at 99.5% (1841 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/it/
- Chore: Translated using Weblate (Portuguese (Portugal))
Currently translated at 33.4% (619 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/pt_PT/
- Chore: Merge branch 'origin/develop' into Weblate.
- Chore: Translated using Weblate (Portuguese (Portugal))
Currently translated at 33.4% (619 of 1850 strings)
Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/pt_PT/
- Chore: Merge branch 'origin/develop' into Weblate.
### Performance ### Performance
- Perf: :zap: featured posts query limit - Perf: :zap: featured posts query limit
- Perf: :zap: delete transformedOptions key -> assign undefined with key literal
- Perf: :zap: seperate web and queue workers
### Refactor ### Refactor
@ -778,6 +1311,26 @@ ref: https://git.joinfirefish.org/firefish/firefish/-/issues/10527#note_230
- Refactor: :busts_in_silhouette: Add original Misskey contributors - Refactor: :busts_in_silhouette: Add original Misskey contributors
- Refactor: :recycle: better offline page
- Refactor: :children_crossing: only index public posts
- Refactor: :coffin: remove old woodpecker scripts
- Refactor: :recycle: No Vue Reactivity
Performed with https://github.com/edison1105/drop-reactivity-transform , Reactivity Transform was an experimental feature and has now been deprecated.
- Refactor: remove regex
- Refactor: :recycle: types in AP kernel
This file seriously needs to be refactored properly...
- Refactor: :egg: new ansi art
- Refactor: :egg: ansi art for master.ts
### Styling ### Styling
@ -797,6 +1350,20 @@ ref: https://git.joinfirefish.org/firefish/firefish/-/issues/10527#note_230
### Miscellaneous Tasks ### Miscellaneous Tasks
- Chore: Translated using Weblate (Russian)
Currently translated at 99.4% (1826 of 1836 strings)
Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ru/
- Chore: Translated using Weblate (Russian)
Currently translated at 99.4% (1826 of 1836 strings)
Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ru/
- Chore: Translated using Weblate (Japanese) - Chore: Translated using Weblate (Japanese)
Currently translated at 100.0% (1836 of 1836 strings) Currently translated at 100.0% (1836 of 1836 strings)
@ -891,7 +1458,7 @@ Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ko/
### Refactor ### Refactor
- Refactor: :recycle: create-drive-file endpoint - Refactor: :recycle: create drive file endpoint
Adjusts ratelimit to 250 files every 10 minutes, fixes error text, fixes reused variable name. Adjusts ratelimit to 250 files every 10 minutes, fixes error text, fixes reused variable name.
@ -901,22 +1468,6 @@ Adjusts ratelimit to 250 files every 10 minutes, fixes error text, fixes reused
for real this time for real this time
### Miscellaneous Tasks
- Chore: Translated using Weblate (Russian)
Currently translated at 99.4% (1826 of 1836 strings)
Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ru/
- Chore: Translated using Weblate (Russian)
Currently translated at 99.4% (1826 of 1836 strings)
Translation: Calckey/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ru/
## [1.0.0] - 2023-07-19 ## [1.0.0] - 2023-07-19
@ -8900,7 +9451,7 @@ Resolve #7540
* truncate user information if it is too long * truncate user information if it is too long
Some AP software allows for user names or summaries to be very long. Some AP software allows for user names or summaries to be very long.
Misskey cannot handle this and the profile page cannot be opened and Misskey can not handle this and the profile page can not be opened and
no activities from such users can be seen. no activities from such users can be seen.
Instead, the user name and summary are cut off after the maximum length Instead, the user name and summary are cut off after the maximum length
@ -9902,7 +10453,7 @@ This duplicated processing can be avoided by querying the database directly.
Misskey will only use ActivityPub follow requests for users that are local Misskey will only use ActivityPub follow requests for users that are local
and are requesting to follow a remote user. This check is to ensure that and are requesting to follow a remote user. This check is to ensure that
this endpoint cannot be used by other services or instances. this endpoint can not be used by other services or instances.
* fix: missing import * fix: missing import
@ -14921,7 +15472,7 @@ Defaults for `local` and `withFiles` are based on the behaviour of the endpoint.
* fix: define required fields * fix: define required fields
- `notes/create`: the default for `text` has been removed because ajv cannot handle - `notes/create`: the default for `text` has been removed because ajv can not handle
`default` inside of `anyOf`, see `default` inside of `anyOf`, see
https://ajv.js.org/guide/modifying-data.html#assigning-defaults https://ajv.js.org/guide/modifying-data.html#assigning-defaults
and the default value cannot be `null` if text is `nullable: false` in the `anyOf` and the default value cannot be `null` if text is `nullable: false` in the `anyOf`
@ -15551,7 +16102,7 @@ unnecessarily loaded.
* remove duplicate null check * remove duplicate null check
The variable is checked for null in the lines above and the function The variable is checked for null in the lines above and the function
returns if so. Therefore, it cannot be null at this point. returns if so. Therefore, it can not be null at this point.
* simplify `getJsonSchema` * simplify `getJsonSchema`

View file

@ -23,31 +23,32 @@ Before creating an issue, please check the following:
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
## Before implementation ## Before implementation
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented. When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the MR will not be merged even if it is implemented.
At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them. At this point, you also need to clarify the goals of the MR you will create, and make sure that the other members of the team are aware of them.
PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review. MRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work.
## Well-known branches ## Well-known branches
- The **`main`** branch is tracking the latest release and used for production purposes. - The **`main`** branch is tracking the latest release and used for production purposes.
- The **`develop`** branch is where we work for the next release. - The **`develop`** branch is where we work for the next release.
- When you create a PR, basically target it to this branch. **But create a different branch** - When you create a MR, basically target it to this branch. **But create a different branch**
- The **`l10n_develop`** branch is reserved for localization management. - The **`l10n_develop`** branch is reserved for localization management.
- **`feature/*`** branches are reserved for the development of a specific feature - **`feature/*`** branches are reserved for the development of a specific feature
## Creating a PR ## Creating a merge request (MR)
Thank you for your PR! Before creating a PR, please check the following: Thank you for your MR! Before creating a MR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - If possible, prefix the title with a keyword that identifies the type of this MR, as shown below.
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc. You are also welcome to use gitmoji. This is important as we use these to A) easier read the git history and B) generate our changelog. Without propper prefixing it is possible that your PR is rejected. - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc. You are also welcome to use gitmoji. This is important as we use these to A) easier read the git history and B) generate our changelog. Without propper prefixing it is possible that your MR is rejected.
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - Also, make sure that the granularity of this MR is appropriate. Please do not include more than one type of change or interest in a single MR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. Good examples include `Closing: #21` or `Resolves: #21` - If there is an Issue which will be resolved by this MR, please include a reference to the Issue in the text. Good examples include `Closing: #21` or `Resolves: #21`
- Check if there are any documents that need to be created or updated due to this change. - Check if there are any documents that need to be created or updated due to this change.
- For example, you need to update `docs/api-change.md` if the MR includes API changes.
- If you have added a feature or fixed a bug, please add a test case if possible. - If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that formatting, tests and Lint are passed in advance. - Please make sure that formatting, tests and Lint are passed in advance.
- You can run it with `pnpm run format`, `pnpm run test` and `pnpm run lint`. [See more info](#testing) - You can run it with `pnpm run format`, `pnpm run test` and `pnpm run lint`. [See more info](#testing)
- If this PR includes UI changes, please attach a screenshot in the text. - If this MR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗 Thanks for your cooperation 🤗
@ -56,12 +57,12 @@ Be willing to comment on the good points and not just the things you want fixed
### Review perspective ### Review perspective
- Scope - Scope
- Are the goals of the PR clear? - Are the goals of the MR clear?
- Is the granularity of the PR appropriate? - Is the granularity of the MR appropriate?
- Security - Security
- Does merging this PR create a vulnerability? - Does merging this MR create a vulnerability?
- Performance - Performance
- Will merging this PR cause unexpected performance degradation? - Will merging this MR cause unexpected performance degradation?
- Is there a more efficient way? - Is there a more efficient way?
- Testing - Testing
- Does the test ensure the expected behavior? - Does the test ensure the expected behavior?
@ -69,12 +70,14 @@ Be willing to comment on the good points and not just the things you want fixed
- Does it check for anomalies? - Does it check for anomalies?
## Deploy (SOON) ## Deploy (SOON)
The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment. The `/deploy` command by issue comment can be used to deploy the contents of a MR to the preview environment.
``` ```
/deploy sha=<commit hash> /deploy sha=<commit hash>
``` ```
An actual domain will be assigned so you can test the federation. An actual domain will be assigned so you can test the federation.
# THE FOLLOWING IS OUTDATED:
## Merge ## Merge
## Release ## Release
@ -95,9 +98,6 @@ During development, it is useful to use the `yarn dev` command.
This command monitors the server-side and client-side source files and automatically builds them if they are modified. This command monitors the server-side and client-side source files and automatically builds them if they are modified.
In addition, it will also automatically start the Misskey server process. In addition, it will also automatically start the Misskey server process.
# THE FOLLOWING IS OUTDATED:
## Testing ## Testing
- Test codes are located in [`/test`](/test). - Test codes are located in [`/test`](/test).

View file

@ -209,6 +209,7 @@ Please don't use ElasticSearch unless you already have an ElasticSearch setup an
- To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there. - To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there.
- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory. - To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory.
- To update custom assets without rebuilding, just run `pnpm run gulp`. - To update custom assets without rebuilding, just run `pnpm run gulp`.
- To block ChatGPT, CommonCrawl, or other crawlers from indexing your instance, uncomment the respective rules in `./custom/robots.txt`.
## 🧑‍🔬 Configuring a new server ## 🧑‍🔬 Configuring a new server

14
custom/assets/robots.txt Normal file
View file

@ -0,0 +1,14 @@
User-agent: *
Allow: /
# Uncomment the following to block CommonCrawl
#
# User-agent: CCBot
# User-agent: CCBot/2.0
# User-agent: CCBot/3.1
# Disallow: /
# Uncomment the following to block ChatGPT
#
# User-agent: GPTBot
# Disallow: /

7
docs/api-change.md Normal file
View file

@ -0,0 +1,7 @@
# Changes to the Firefish API
## v1.0.5 (unreleased)
### dev11
- `notes/translate` now requires credentials.

View file

@ -762,8 +762,7 @@ no: "No"
driveFilesCount: "Number of Drive files" driveFilesCount: "Number of Drive files"
driveUsage: "Drive space usage" driveUsage: "Drive space usage"
noCrawle: "Reject crawler indexing" noCrawle: "Reject crawler indexing"
noCrawleDescription: "Ask search engines to not index your profile page, posts, Pages, noCrawleDescription: "Ask external search engines to not index your content."
etc."
lockedAccountInfo: "Unless you set your post visiblity to \"Followers only\", your lockedAccountInfo: "Unless you set your post visiblity to \"Followers only\", your
posts will be visible to anyone, even if you require followers to be manually approved." posts will be visible to anyone, even if you require followers to be manually approved."
alwaysMarkSensitive: "Mark as NSFW by default" alwaysMarkSensitive: "Mark as NSFW by default"
@ -1139,6 +1138,10 @@ confirm: "Confirm"
importZip: "Import ZIP" importZip: "Import ZIP"
exportZip: "Export ZIP" exportZip: "Export ZIP"
emojiPackCreator: "Emoji pack creator" emojiPackCreator: "Emoji pack creator"
indexable: "Indexable"
indexableDescription: "Allow built-in search to show your public posts"
languageForTranslation: "Post translation language"
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing description: "Reduces the effort of server moderation through automatically recognizing

View file

@ -874,7 +874,7 @@ pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación" lastCommunication: "Última comunicación"
resolved: "Resuelto" resolved: "Resuelto"
unresolved: "Sin resolver" unresolved: "Sin resolver"
breakFollow: "Dejar de seguir" breakFollow: "Quitar seguidor"
itsOn: "¡Está encendido!" itsOn: "¡Está encendido!"
itsOff: "¡Está apagado!" itsOff: "¡Está apagado!"
emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro emailRequiredForSignup: "Se requere una dirección de correo electrónico para el registro

View file

@ -309,11 +309,11 @@ emptyDrive: "Le Drive est vide"
emptyFolder: "Le dossier est vide" emptyFolder: "Le dossier est vide"
unableToDelete: "Suppression impossible" unableToDelete: "Suppression impossible"
inputNewFileName: "Entrez un nouveau nom de fichier" inputNewFileName: "Entrez un nouveau nom de fichier"
inputNewDescription: "Veuillez entrer une nouvelle description" inputNewDescription: "Veuillez entrer une nouvelle description au fichier"
inputNewFolderName: "Entrez un nouveau nom de dossier" inputNewFolderName: "Entrez un nouveau nom de dossier"
circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier
que vous souhaitez déplacer." que vous souhaitez déplacer."
hasChildFilesOrFolders: "Impossible de supprimer ce dossier car il n'est pas vide." hasChildFilesOrFolders: "Impossible de supprimer ce dossier, car il n'est pas vide."
copyUrl: "Copier lURL" copyUrl: "Copier lURL"
rename: "Renommer" rename: "Renommer"
avatar: "Avatar" avatar: "Avatar"
@ -605,7 +605,7 @@ disablePlayer: "Fermer le lecteur vidéo"
expandTweet: "Étendre le tweet" expandTweet: "Étendre le tweet"
themeEditor: "Éditeur de thèmes" themeEditor: "Éditeur de thèmes"
description: "Description" description: "Description"
describeFile: "Ajouter une description d'image" describeFile: "Ajouter une description"
enterFileDescription: "Saisissez une description" enterFileDescription: "Saisissez une description"
author: "Auteur·rice" author: "Auteur·rice"
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer
@ -2085,7 +2085,7 @@ silenceThisInstance: Masquer ce serveur
silencedInstances: Serveurs masqués silencedInstances: Serveurs masqués
silenced: Masqué silenced: Masqué
deleted: Effacé deleted: Effacé
editNote: Modifier publication editNote: Modifier la publication
edited: 'Modifié à {date} {time}' edited: 'Modifié à {date} {time}'
flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponses des flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponses des
utilisatieur·rice·s aux publications des autres. utilisatieur·rice·s aux publications des autres.
@ -2209,4 +2209,4 @@ addRe: Ajouter "re:" au début dun avertissement de contenu (CW) en réponse
confirm: Confirmer confirm: Confirmer
importZip: Importer ZIP importZip: Importer ZIP
exportZip: Exporter ZIP exportZip: Exporter ZIP
emojiPackCreator: Créateur de pack demoji emojiPackCreator: Créateur de pack démoji

View file

@ -157,7 +157,7 @@ flagAsCatDescription: "Ti compariranno le orecchie e parlerai come un gatto!"
autoAcceptFollowed: "Accetta in automatico i follow dagli account che segui" autoAcceptFollowed: "Accetta in automatico i follow dagli account che segui"
addAccount: "Aggiungi account" addAccount: "Aggiungi account"
loginFailed: "Accesso non riuscito" loginFailed: "Accesso non riuscito"
showOnRemote: "Apri la pagina di origine" showOnRemote: "Visita la pagina di origine"
general: "Generali" general: "Generali"
wallpaper: "Sfondo" wallpaper: "Sfondo"
setWallpaper: "Imposta sfondo" setWallpaper: "Imposta sfondo"

View file

@ -988,6 +988,8 @@ youHaveUnreadAnnouncements: "未読のお知らせがあります"
neverShow: "今後表示しない" neverShow: "今後表示しない"
remindMeLater: "また後で" remindMeLater: "また後で"
addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する" addRe: "閲覧注意の投稿への返信で、注釈の先頭に\"re:\"を追加する"
languageForTranslation: "投稿翻訳に使用する言語"
detectPostLanguage: "投稿の言語を自動検出し、外国語の投稿に翻訳ボタンを表示する"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"

View file

@ -1,6 +1,6 @@
{ {
"name": "firefish", "name": "firefish",
"version": "1.0.5-dev8", "version": "1.0.5-dev11",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -6,6 +6,7 @@ mod m20230531_180824_drop_reversi;
mod m20230627_185451_index_note_url; mod m20230627_185451_index_note_url;
mod m20230709_000510_move_antenna_to_cache; mod m20230709_000510_move_antenna_to_cache;
mod m20230806_170616_fix_antenna_stream_ids; mod m20230806_170616_fix_antenna_stream_ids;
mod m20230904_013244_is_indexable;
pub struct Migrator; pub struct Migrator;
@ -17,6 +18,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230627_185451_index_note_url::Migration), Box::new(m20230627_185451_index_note_url::Migration),
Box::new(m20230709_000510_move_antenna_to_cache::Migration), Box::new(m20230709_000510_move_antenna_to_cache::Migration),
Box::new(m20230806_170616_fix_antenna_stream_ids::Migration), Box::new(m20230806_170616_fix_antenna_stream_ids::Migration),
Box::new(m20230904_013244_is_indexable::Migration),
] ]
} }
} }

View file

@ -0,0 +1,74 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(User::Table)
.add_column(
ColumnDef::new(User::IsIndexable)
.boolean()
.not_null()
.default(true),
)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(UserProfile::Table)
.add_column(
ColumnDef::new(UserProfile::IsIndexable)
.boolean()
.not_null()
.default(true),
)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(User::Table)
.drop_column(User::IsIndexable)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(UserProfile::Table)
.drop_column(UserProfile::IsIndexable)
.to_owned(),
)
.await?;
Ok(())
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum User {
Table,
#[iden = "isIndexable"]
IsIndexable,
}
#[derive(Iden)]
enum UserProfile {
Table,
#[iden = "isIndexable"]
IsIndexable,
}

View file

@ -71,6 +71,8 @@ pub struct Model {
pub also_known_as: Option<String>, pub also_known_as: Option<String>,
#[sea_orm(column_name = "speakAsCat")] #[sea_orm(column_name = "speakAsCat")]
pub speak_as_cat: bool, pub speak_as_cat: bool,
#[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -75,6 +75,8 @@ pub struct Model {
pub moderation_note: String, pub moderation_note: String,
#[sea_orm(column_name = "preventAiLearning")] #[sea_orm(column_name = "preventAiLearning")]
pub prevent_ai_learning: bool, pub prevent_ai_learning: bool,
#[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -19,7 +19,12 @@ const ev = new Xev();
* Init process * Init process
*/ */
export default async function () { export default async function () {
process.title = `Firefish (${cluster.isPrimary ? "master" : "worker"})`; const mode =
process.env.mode && ["web", "queue"].includes(process.env.mode)
? `(${process.env.mode})`
: "";
const type = cluster.isPrimary ? "(master)" : "(worker)";
process.title = `Firefish ${mode} ${type}`;
if (cluster.isPrimary || envOption.disableClustering) { if (cluster.isPrimary || envOption.disableClustering) {
await masterMain(); await masterMain();

View file

@ -111,7 +111,7 @@ export async function masterMain() {
bootLogger.succ("Firefish initialized"); bootLogger.succ("Firefish initialized");
if (!envOption.disableClustering) { if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit); await spawnWorkers(config.clusterLimits);
} }
bootLogger.succ( bootLogger.succ(
@ -120,7 +120,11 @@ export async function masterMain() {
true, true,
); );
if (!envOption.noDaemons && !config.onlyQueueProcessor) { if (
!envOption.noDaemons &&
config.clusterLimits?.web &&
config.clusterLimits?.web >= 1
) {
import("../daemons/server-stats.js").then((x) => x.default()); import("../daemons/server-stats.js").then((x) => x.default());
import("../daemons/queue-stats.js").then((x) => x.default()); import("../daemons/queue-stats.js").then((x) => x.default());
import("../daemons/janitor.js").then((x) => x.default()); import("../daemons/janitor.js").then((x) => x.default());
@ -136,7 +140,7 @@ function showEnvironment(): void {
if (env !== "production") { if (env !== "production") {
logger.warn("The environment is not in production mode."); logger.warn("The environment is not in production mode.");
logger.warn("DO NOT USE FOR PRODUCTION PURPOSE!", null, true); logger.warn("DO NOT USE THIS IN PRODUCTION!", null, true);
} }
} }
@ -194,19 +198,35 @@ async function connectDb(): Promise<void> {
} }
} }
async function spawnWorkers(limit = 1) { async function spawnWorkers(
const workers = Math.min(limit, os.cpus().length); clusterLimits: Required<Config["clusterLimits"]>,
bootLogger.info(`Starting ${workers} worker${workers === 1 ? "" : "s"}...`); ): Promise<void> {
await Promise.all([...Array(workers)].map(spawnWorker)); const modes = ["web", "queue"];
const cpus = os.cpus().length;
for (const mode of modes.filter((mode) => clusterLimits[mode] > cpus)) {
bootLogger.warn(
`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`,
);
}
const total = modes.reduce((acc, mode) => acc + clusterLimits[mode], 0);
const workers = new Array(total);
workers.fill("web", 0, clusterLimits?.web);
workers.fill("queue", clusterLimits?.web);
bootLogger.info(
`Starting ${clusterLimits?.web} web workers and ${clusterLimits?.queue} queue workers (total ${total})...`,
);
await Promise.all(workers.map((mode) => spawnWorker(mode)));
bootLogger.succ("All workers started"); bootLogger.succ("All workers started");
} }
function spawnWorker(): Promise<void> { function spawnWorker(mode: "web" | "queue"): Promise<void> {
return new Promise((res) => { return new Promise((res) => {
const worker = cluster.fork(); const worker = cluster.fork({ mode });
worker.on("message", (message) => { worker.on("message", (message) => {
if (message === "listenFailed") { if (message === "listenFailed") {
bootLogger.error("The server Listen failed due to the previous error."); bootLogger.error("The server listen failed due to the previous error.");
process.exit(1); process.exit(1);
} }
if (message !== "ready") return; if (message !== "ready") return;

View file

@ -1,6 +1,7 @@
import cluster from "node:cluster"; import cluster from "node:cluster";
import { initDb } from "../db/postgre.js"; import { initDb } from "../db/postgre.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import os from "node:os";
/** /**
* Init worker process * Init worker process
@ -8,13 +9,20 @@ import config from "@/config/index.js";
export async function workerMain() { export async function workerMain() {
await initDb(); await initDb();
if (!config.onlyQueueProcessor) { if (!process.env.mode || process.env.mode === "web") {
// start server // start server
await import("../server/index.js").then((x) => x.default()); await import("../server/index.js").then((x) => x.default());
} }
// start job queue if (!process.env.mode || process.env.mode === "queue") {
import("../queue/index.js").then((x) => x.default()); // start job queue
import("../queue/index.js").then((x) => x.default());
if (process.env.mode === "queue") {
// if this is an exclusive queue worker, renice to have higher priority
os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL);
}
}
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process

View file

@ -59,6 +59,23 @@ export default function load() {
if (config.cacheServer && !config.cacheServer.prefix) if (config.cacheServer && !config.cacheServer.prefix)
config.cacheServer.prefix = mixin.hostname; config.cacheServer.prefix = mixin.hostname;
if (!config.clusterLimits) {
config.clusterLimits = {
web: 1,
queue: 1,
};
} else {
config.clusterLimits = {
web: 1,
queue: 1,
...config.clusterLimits,
};
if (config.clusterLimits.web! < 1 || config.clusterLimits.queue! < 1) {
throw new Error("Invalid cluster limits");
}
}
return Object.assign(config, mixin); return Object.assign(config, mixin);
} }

View file

@ -77,9 +77,10 @@ export type Source = {
accesslog?: string; accesslog?: string;
clusterLimit?: number; clusterLimits?: {
web?: number;
onlyQueueProcessor?: boolean; queue?: number;
};
cuid?: { cuid?: {
length?: number; length?: number;

View file

@ -1,2 +1,2 @@
// biome-ignore lint/suspicious/noExplicitAny: i have no idea // rome-ignore lint/suspicious/noExplicitAny: i have no idea
type FIXME = any; type FIXME = any;

View file

@ -12,29 +12,38 @@ const retryDelay = 100;
* @param timeout Lock timeout (ms), The timeout releases previous lock. * @param timeout Lock timeout (ms), The timeout releases previous lock.
* @returns Unlock function * @returns Unlock function
*/ */
export async function getApLock(uri: string, timeout = 30 * 1000) { export async function getApLock(
uri: string,
timeout = 30 * 1000,
): Promise<Mutex> {
const lock = new Mutex(redisClient, `ap-object:${uri}`, { const lock = new Mutex(redisClient, `ap-object:${uri}`, {
lockTimeout: timeout, lockTimeout: timeout,
retryInterval: retryDelay, retryInterval: retryDelay,
}); });
await lock.acquire(); await lock.acquire();
return lock;
} }
export async function getFetchInstanceMetadataLock( export async function getFetchInstanceMetadataLock(
host: string, host: string,
timeout = 30 * 1000, timeout = 30 * 1000,
) { ): Promise<Mutex> {
const lock = new Mutex(redisClient, `instance:${host}`, { const lock = new Mutex(redisClient, `instance:${host}`, {
lockTimeout: timeout, lockTimeout: timeout,
retryInterval: retryDelay, retryInterval: retryDelay,
}); });
await lock.acquire(); await lock.acquire();
return lock;
} }
export async function getChartInsertLock(lockKey: string, timeout = 30 * 1000) { export async function getChartInsertLock(
lockKey: string,
timeout = 30 * 1000,
): Promise<Mutex> {
const lock = new Mutex(redisClient, `chart-insert:${lockKey}`, { const lock = new Mutex(redisClient, `chart-insert:${lockKey}`, {
lockTimeout: timeout, lockTimeout: timeout,
retryInterval: retryDelay, retryInterval: retryDelay,
}); });
await lock.acquire(); await lock.acquire();
return lock;
} }

View file

@ -167,6 +167,12 @@ export class UserProfile {
}) })
public noCrawle: boolean; public noCrawle: boolean;
@Column("boolean", {
default: true,
comment: "Whether User is indexable.",
})
public isIndexable: boolean;
@Column("boolean", { @Column("boolean", {
default: true, default: true,
}) })

View file

@ -265,6 +265,13 @@ export class User {
}) })
public driveCapacityOverrideMb: number | null; public driveCapacityOverrideMb: number | null;
@Index()
@Column("boolean", {
default: true,
comment: "Whether the User is indexable.",
})
public isIndexable: boolean;
constructor(data: Partial<User>) { constructor(data: Partial<User>) {
if (data == null) return; if (data == null) return;

View file

@ -474,6 +474,7 @@ export const UserRepository = db.getRepository(User).extend({
isModerator: user.isModerator || falsy, isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy, isBot: user.isBot || falsy,
isLocked: user.isLocked, isLocked: user.isLocked,
isIndexable: user.isIndexable,
isCat: user.isCat || falsy, isCat: user.isCat || falsy,
speakAsCat: user.speakAsCat || falsy, speakAsCat: user.speakAsCat || falsy,
instance: user.host instance: user.host

View file

@ -66,6 +66,11 @@ export const packedUserLiteSchema = {
nullable: false, nullable: false,
optional: true, optional: true,
}, },
isIndexable: {
type: "boolean",
nullable: false,
optional: true,
},
speakAsCat: { speakAsCat: {
type: "boolean", type: "boolean",
nullable: false, nullable: false,

View file

@ -32,6 +32,8 @@ export default async function (
// Interrupt if you block the announcement destination // Interrupt if you block the announcement destination
if (await shouldBlockInstance(extractDbHost(uri))) return; if (await shouldBlockInstance(extractDbHost(uri))) return;
const lock = await getApLock(uri);
try { try {
// Check if something with the same URI is already registered // Check if something with the same URI is already registered
const exist = await fetchNote(uri); const exist = await fetchNote(uri);
@ -78,6 +80,6 @@ export default async function (
uri, uri,
}); });
} finally { } finally {
await getApLock(uri); await lock.release();
} }
} }

View file

@ -31,6 +31,8 @@ export default async function (
} }
} }
const lock = await getApLock(uri);
try { try {
const exist = await fetchNote(note); const exist = await fetchNote(note);
if (exist) return "skip: note exists"; if (exist) return "skip: note exists";
@ -44,6 +46,6 @@ export default async function (
throw e; throw e;
} }
} finally { } finally {
await getApLock(uri); await lock.release();
} }
} }

View file

@ -13,6 +13,8 @@ export default async function (
): Promise<string> { ): Promise<string> {
logger.info(`Deleting the Note: ${uri}`); logger.info(`Deleting the Note: ${uri}`);
const lock = await getApLock(uri);
try { try {
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
const note = await dbResolver.getNoteFromApId(uri); const note = await dbResolver.getNoteFromApId(uri);
@ -37,6 +39,6 @@ export default async function (
await deleteNode(actor, note); await deleteNode(actor, note);
return "ok: note deleted"; return "ok: note deleted";
} finally { } finally {
await getApLock(uri); await lock.release();
} }
} }

View file

@ -454,6 +454,8 @@ export async function resolveNote(
`host ${extractDbHost(uri)} is blocked`, `host ${extractDbHost(uri)} is blocked`,
); );
const lock = await getApLock(uri);
try { try {
//#region Returns if already registered with this server //#region Returns if already registered with this server
const exist = await fetchNote(uri); const exist = await fetchNote(uri);
@ -476,7 +478,7 @@ export async function resolveNote(
// Since the attached Note Object may be disguised, always specify the uri and fetch it from the server. // Since the attached Note Object may be disguised, always specify the uri and fetch it from the server.
return await createNote(uri, resolver, true); return await createNote(uri, resolver, true);
} finally { } finally {
await getApLock(uri); await lock.release();
} }
} }

View file

@ -209,10 +209,10 @@ export async function createPerson(
if (typeof person.followers === "string") { if (typeof person.followers === "string") {
try { try {
let data = await fetch(person.followers, { const data = await fetch(person.followers, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
let json_data = JSON.parse(await data.text()); const json_data = JSON.parse(await data.text());
followersCount = json_data.totalItems; followersCount = json_data.totalItems;
} catch { } catch {
@ -224,10 +224,10 @@ export async function createPerson(
if (typeof person.following === "string") { if (typeof person.following === "string") {
try { try {
let data = await fetch(person.following, { const data = await fetch(person.following, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
let json_data = JSON.parse(await data.text()); const json_data = JSON.parse(await data.text());
followingCount = json_data.totalItems; followingCount = json_data.totalItems;
} catch (e) { } catch (e) {
@ -239,10 +239,10 @@ export async function createPerson(
if (typeof person.outbox === "string") { if (typeof person.outbox === "string") {
try { try {
let data = await fetch(person.outbox, { const data = await fetch(person.outbox, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });
let json_data = JSON.parse(await data.text()); const json_data = JSON.parse(await data.text());
notesCount = json_data.totalItems; notesCount = json_data.totalItems;
} catch (e) { } catch (e) {
@ -306,6 +306,7 @@ export async function createPerson(
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isIndexable: person.indexable,
}), }),
)) as IRemoteUser; )) as IRemoteUser;
@ -555,6 +556,7 @@ export async function updatePerson(
tags, tags,
isBot: getApType(object) !== "Person", isBot: getApType(object) !== "Person",
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isIndexable: person.indexable,
isLocked: !!person.manuallyApprovesFollowers, isLocked: !!person.manuallyApprovesFollowers,
movedToUri: person.movedTo || null, movedToUri: person.movedTo || null,
alsoKnownAs: person.alsoKnownAs || null, alsoKnownAs: person.alsoKnownAs || null,

View file

@ -30,6 +30,7 @@ export const renderActivity = (x: any): IActivity | null => {
Emoji: "toot:Emoji", Emoji: "toot:Emoji",
featured: "toot:featured", featured: "toot:featured",
discoverable: "toot:discoverable", discoverable: "toot:discoverable",
indexable: "toot:indexable",
// schema // schema
schema: "http://schema.org#", schema: "http://schema.org#",
PropertyValue: "schema:PropertyValue", PropertyValue: "schema:PropertyValue",

View file

@ -81,6 +81,7 @@ export async function renderPerson(user: ILocalUser) {
discoverable: !!user.isExplorable, discoverable: !!user.isExplorable,
publicKey: renderKey(user, keypair, "#main-key"), publicKey: renderKey(user, keypair, "#main-key"),
isCat: user.isCat, isCat: user.isCat,
indexable: user.isIndexable,
attachment: attachment.length ? attachment : undefined, attachment: attachment.length ? attachment : undefined,
} as any; } as any;

View file

@ -190,8 +190,9 @@ export interface IActor extends IObject {
movedTo?: string; movedTo?: string;
alsoKnownAs?: string[]; alsoKnownAs?: string[];
discoverable?: boolean; discoverable?: boolean;
indexable?: boolean;
inbox: string; inbox: string;
sharedInbox?: string; // backward compatibility.. ig sharedInbox?: string; // Backwards compatibility
publicKey?: { publicKey?: {
id: string; id: string;
publicKeyPem: string; publicKeyPem: string;

View file

@ -60,6 +60,7 @@ export default define(meta, paramDef, async (ps, me) => {
emailVerified: profile.emailVerified, emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed, autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle, noCrawle: profile.noCrawle,
isIndexable: profile.isIndexable,
preventAiLearning: profile.preventAiLearning, preventAiLearning: profile.preventAiLearning,
alwaysMarkNsfw: profile.alwaysMarkNsfw, alwaysMarkNsfw: profile.alwaysMarkNsfw,
autoSensitive: profile.autoSensitive, autoSensitive: profile.autoSensitive,

View file

@ -121,6 +121,7 @@ export const paramDef = {
isBot: { type: "boolean" }, isBot: { type: "boolean" },
isCat: { type: "boolean" }, isCat: { type: "boolean" },
speakAsCat: { type: "boolean" }, speakAsCat: { type: "boolean" },
isIndexable: { type: "boolean" },
injectFeaturedNote: { type: "boolean" }, injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" }, receiveAnnouncementEmail: { type: "boolean" },
alwaysMarkNsfw: { type: "boolean" }, alwaysMarkNsfw: { type: "boolean" },
@ -207,6 +208,10 @@ export default define(meta, paramDef, async (ps, _user, token) => {
if (typeof ps.preventAiLearning === "boolean") if (typeof ps.preventAiLearning === "boolean")
profileUpdates.preventAiLearning = ps.preventAiLearning; profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat; if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
if (typeof ps.isIndexable === "boolean") {
updates.isIndexable = ps.isIndexable;
profileUpdates.isIndexable = ps.isIndexable;
}
if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat; if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === "boolean") if (typeof ps.injectFeaturedNote === "boolean")
profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;

View file

@ -764,7 +764,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchNote); throw new ApiError(meta.errors.noSuchNote);
} }
if (publishing) { if (publishing && user.isIndexable) {
index(note, true); index(note, true);
// Publish update event for the updated note details // Publish update event for the updated note details

View file

@ -4,7 +4,6 @@ import config from "@/config/index.js";
import { Converter } from "opencc-js"; import { Converter } from "opencc-js";
import { getAgentByUrl } from "@/misc/fetch.js"; import { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { Notes } from "@/models/index.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js"; import { getNote } from "../../common/getters.js";
import define from "../../define.js"; import define from "../../define.js";
@ -12,7 +11,7 @@ import define from "../../define.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],
requireCredential: false, requireCredential: true,
requireCredentialPrivateMode: true, requireCredentialPrivateMode: true,
res: { res: {

View file

@ -7,7 +7,7 @@
"display": "standalone", "display": "standalone",
"background_color": "#1f1d2e", "background_color": "#1f1d2e",
"theme_color": "#31748f", "theme_color": "#31748f",
"orientation": "any", "orientation": "natural",
"icons": [ "icons": [
{ {
"src": "/static-assets/icons/192.png", "src": "/static-assets/icons/192.png",
@ -22,7 +22,7 @@
"purpose": "any" "purpose": "any"
}, },
{ {
"src": "/static-assets/icons/512.png", "src": "/static-assets/icons/maskable.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "maskable" "purpose": "maskable"

View file

@ -24,9 +24,11 @@ block meta
unless privateMode unless privateMode
if profile.noCrawle if profile.noCrawle
meta(name='robots' content='noindex') meta(name='robots' content='noindex')
if profile.preventAiLearning if profile.preventAiLearning
meta(name='robots' content='noai') meta(name='robots' content='noai')
meta(name='robots' content='noimageai') meta(name='robots' content='noimageai')
meta(name='GPTBot' content='noindex')
meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)

View file

@ -430,6 +430,7 @@ export default abstract class Chart<T extends Schema> {
? `${this.name}:${date}:${span}:${group}` ? `${this.name}:${date}:${span}:${group}`
: `${this.name}:${date}:${span}`; : `${this.name}:${date}:${span}`;
const lock = await getChartInsertLock(lockKey);
try { try {
// ロック内でもう1回チェックする // ロック内でもう1回チェックする
const currentLog = (await repository.findOneBy({ const currentLog = (await repository.findOneBy({
@ -465,14 +466,14 @@ export default abstract class Chart<T extends Schema> {
return log; return log;
} finally { } finally {
await getChartInsertLock(lockKey); await lock.release();
} }
} }
protected commit(diff: Commit<T>, group: string | null = null): void { protected commit(diff: Commit<T>, group: string | null = null): void {
for (const [k, v] of Object.entries(diff)) { for (const [k, v] of Object.entries(diff)) {
if (v == null || v === 0 || (Array.isArray(v) && v.length === 0)) if (v == null || v === 0 || (Array.isArray(v) && v.length === 0))
// biome-ignore lint/performance/noDelete: needs to be deleted not just set to undefined // rome-ignore lint/performance/noDelete: needs to be deleted not just set to undefined
delete diff[k]; delete diff[k];
} }
this.buffer.push({ this.buffer.push({

View file

@ -15,6 +15,8 @@ export async function fetchInstanceMetadata(
instance: Instance, instance: Instance,
force = false, force = false,
): Promise<void> { ): Promise<void> {
const lock = await getFetchInstanceMetadataLock(instance.host);
if (!force) { if (!force) {
const _instance = await Instances.findOneBy({ host: instance.host }); const _instance = await Instances.findOneBy({ host: instance.host });
const now = Date.now(); const now = Date.now();
@ -22,7 +24,7 @@ export async function fetchInstanceMetadata(
_instance?.infoUpdatedAt && _instance?.infoUpdatedAt &&
now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24 now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24
) { ) {
await getFetchInstanceMetadataLock(instance.host); await lock.release();
return; return;
} }
} }
@ -78,7 +80,7 @@ export async function fetchInstanceMetadata(
} catch (e) { } catch (e) {
logger.error(`Failed to update metadata of ${instance.host}: ${e}`); logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
} finally { } finally {
await getFetchInstanceMetadataLock(instance.host); await lock.release();
} }
} }

View file

@ -173,11 +173,12 @@ export default async (
createdAt: User["createdAt"]; createdAt: User["createdAt"];
isBot: User["isBot"]; isBot: User["isBot"];
inbox?: User["inbox"]; inbox?: User["inbox"];
isIndexable?: User["isIndexable"];
}, },
data: Option, data: Option,
silent = false, silent = false,
) => ) =>
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME // rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
new Promise<Note>(async (res, rej) => { new Promise<Note>(async (res, rej) => {
const dontFederateInitially = data.visibility === "hidden"; const dontFederateInitially = data.visibility === "hidden";
@ -666,7 +667,9 @@ export default async (
} }
// Register to search database // Register to search database
await index(note, false); if (user.isIndexable) {
await index(note, false);
}
}); });
async function renderNoteOrRenoteActivity(data: Option, note: Note) { async function renderNoteOrRenoteActivity(data: Option, note: Note) {

View file

@ -151,6 +151,7 @@ describe("ユーザー", () => {
carefulBot: user.carefulBot, carefulBot: user.carefulBot,
autoAcceptFollowed: user.autoAcceptFollowed, autoAcceptFollowed: user.autoAcceptFollowed,
noCrawle: user.noCrawle, noCrawle: user.noCrawle,
isIndexable: user.isIndexable,
preventAiLearning: user.preventAiLearning, preventAiLearning: user.preventAiLearning,
isExplorable: user.isExplorable, isExplorable: user.isExplorable,
isDeleted: user.isDeleted, isDeleted: user.isDeleted,
@ -529,6 +530,8 @@ describe("ユーザー", () => {
{ parameters: (): object => ({ autoAcceptFollowed: false }) }, { parameters: (): object => ({ autoAcceptFollowed: false }) },
{ parameters: (): object => ({ noCrawle: true }) }, { parameters: (): object => ({ noCrawle: true }) },
{ parameters: (): object => ({ noCrawle: false }) }, { parameters: (): object => ({ noCrawle: false }) },
{ parameters: (): object => ({ isIndexable: true }) },
{ parameters: (): object => ({ isIndexable: false }) },
{ parameters: (): object => ({ preventAiLearning: false }) }, { parameters: (): object => ({ preventAiLearning: false }) },
{ parameters: (): object => ({ preventAiLearning: true }) }, { parameters: (): object => ({ preventAiLearning: true }) },
{ parameters: (): object => ({ isBot: true }) }, { parameters: (): object => ({ isBot: true }) },

View file

@ -63,7 +63,7 @@
"katex": "0.16.8", "katex": "0.16.8",
"matter-js": "0.19.0", "matter-js": "0.19.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"photoswipe": "5.3.8", "photoswipe": "5.3.9",
"prettier": "3.0.3", "prettier": "3.0.3",
"prettier-plugin-vue": "1.1.6", "prettier-plugin-vue": "1.1.6",
"prismjs": "1.29.0", "prismjs": "1.29.0",
@ -81,6 +81,7 @@
"three": "0.156.0", "three": "0.156.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tinyld": "^1.3.4",
"tsc-alias": "1.8.7", "tsc-alias": "1.8.7",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",

View file

@ -8,8 +8,10 @@
<div :class="$style.time"> <div :class="$style.time">
<MkTime :time="announcement.createdAt" /> <MkTime :time="announcement.createdAt" />
<div v-if="announcement.updatedAt"> <div v-if="announcement.updatedAt">
{{ i18n.ts.updatedAt }}: <small>
<MkTime :time="announcement.createdAt" /> {{ i18n.ts.updatedAt }}:
<MkTime :time="announcement.createdAt" />
</small>
</div> </div>
</div> </div>
<Mfm :text="text" /> <Mfm :text="text" />
@ -80,6 +82,6 @@ const gotIt = () => {
} }
.gotIt { .gotIt {
margin: 8px 0 0 0; margin: 1rem 0 1rem 2rem;
} }
</style> </style>

View file

@ -111,7 +111,7 @@
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
<div v-else class="translated"> <div v-else-if="translation != null" class="translated">
<b <b
>{{ >{{
i18n.t("translatedFrom", { i18n.t("translatedFrom", {
@ -219,6 +219,18 @@
<i class="ph-minus ph-bold ph-lg"></i> <i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<XQuoteButton class="button" :note="appearNote" /> <XQuoteButton class="button" :note="appearNote" />
<button
v-if="
$i != null &&
isForeignLanguage &&
translation == null
"
class="button _button"
@click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
</button>
<button <button
ref="menuButton" ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more" v-tooltip.noDelay.bottom="i18n.ts.more"
@ -259,6 +271,7 @@ import { computed, inject, onMounted, ref } from "vue";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import type { Ref } from "vue"; import type { Ref } from "vue";
import type * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import { detect as detectLanguage_ } from "tinyld";
import MkSubNoteContent from "./MkSubNoteContent.vue"; import MkSubNoteContent from "./MkSubNoteContent.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
@ -346,6 +359,57 @@ const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const lang = localStorage.getItem("lang");
const translateLang = localStorage.getItem("translateLang");
function detectLanguage(text: string) {
const nodes = mfm.parse(text);
const filtered = mfm.extract(nodes, (node) => {
return node.type === "text" || node.type === "quote";
});
const purified = mfm.toString(filtered);
return detectLanguage_(purified);
}
const isForeignLanguage: boolean =
defaultStore.state.detectPostLanguage &&
appearNote.value.text != null &&
(() => {
const targetLang = (translateLang || lang || navigator.language)?.slice(
0,
2,
);
const postLang = detectLanguage(appearNote.value.text);
return postLang !== "" && postLang !== targetLang;
})();
async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", {
noteId: noteId,
targetLang: targetLang,
});
}
async function translate() {
if (translation.value != null) return;
translating.value = true;
translation.value = await translate_(
appearNote.value.id,
translateLang || lang || navigator.language,
);
// use UI language as the second translation language
if (
translateLang != null &&
lang != null &&
translateLang !== lang &&
(!translation.value ||
translation.value.sourceLang.toLowerCase() ===
translateLang.slice(0, 2))
)
translation.value = await translate_(appearNote.value.id, lang);
translating.value = false;
}
const keymap = { const keymap = {
r: () => reply(true), r: () => reply(true),

View file

@ -124,6 +124,18 @@
<i class="ph-minus ph-bold ph-lg"></i> <i class="ph-minus ph-bold ph-lg"></i>
</button> </button>
<XQuoteButton class="button" :note="appearNote" /> <XQuoteButton class="button" :note="appearNote" />
<button
v-if="
$i != null &&
isForeignLanguage &&
translation == null
"
class="button _button"
@click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
</button>
<button <button
ref="menuButton" ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more" v-tooltip.noDelay.bottom="i18n.ts.more"
@ -180,6 +192,8 @@
import { computed, inject, ref } from "vue"; import { computed, inject, ref } from "vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import type * as misskey from "firefish-js"; import type * as misskey from "firefish-js";
import * as mfm from "mfm-js";
import { detect as detectLanguage_ } from "tinyld";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue"; import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
@ -266,6 +280,57 @@ const replies: misskey.entities.Note[] =
.reverse() ?? []; .reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions; const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick; const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const lang = localStorage.getItem("lang");
const translateLang = localStorage.getItem("translateLang");
function detectLanguage(text: string) {
const nodes = mfm.parse(text);
const filtered = mfm.extract(nodes, (node) => {
return node.type === "text" || node.type === "quote";
});
const purified = mfm.toString(filtered);
return detectLanguage_(purified);
}
const isForeignLanguage: boolean =
defaultStore.state.detectPostLanguage &&
appearNote.value.text != null &&
(() => {
const targetLang = (translateLang || lang || navigator.language)?.slice(
0,
2,
);
const postLang = detectLanguage(appearNote.value.text);
return postLang !== "" && postLang !== targetLang;
})();
async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", {
noteId: noteId,
targetLang: targetLang,
});
}
async function translate() {
if (translation.value != null) return;
translating.value = true;
translation.value = await translate_(
appearNote.value.id,
translateLang || lang || navigator.language,
);
// use UI language as the second translation language
if (
translateLang != null &&
lang != null &&
translateLang !== lang &&
(!translation.value ||
translation.value.sourceLang.toLowerCase() ===
translateLang.slice(0, 2))
)
translation.value = await translate_(appearNote.value.id, lang);
translating.value = false;
}
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: el,

View file

@ -16,8 +16,12 @@
class="announcement _panel" class="announcement _panel"
> >
<div class="_title"> <div class="_title">
<span v-if="$i && !announcement.isRead">🆕 </span> <h3>
<h3>{{ announcement.title }}</h3> <span v-if="$i && !announcement.isRead">
🆕&nbsp;
</span>
{{ announcement.title }}
</h3>
<MkTime :time="announcement.createdAt" /> <MkTime :time="announcement.createdAt" />
<div v-if="announcement.updatedAt"> <div v-if="announcement.updatedAt">
{{ i18n.ts.updatedAt }}: {{ i18n.ts.updatedAt }}:
@ -85,7 +89,7 @@ definePageMetadata({
} }
> ._title { > ._title {
padding: 14px 32px !important; padding: 0.5rem 2rem !important;
} }
> ._seperator { > ._seperator {
@ -93,7 +97,7 @@ definePageMetadata({
} }
> ._content { > ._content {
padding: 2rem; padding: 0 2rem !important;
> img { > img {
display: block; display: block;

View file

@ -45,108 +45,97 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent } from "vue"; import { ref, onMounted } from "vue";
import XForm from "./auth.form.vue"; import XForm from "./auth.form.vue";
import MkSignin from "@/components/MkSignin.vue"; import MkSignin from "@/components/MkSignin.vue";
import MkKeyValue from "@/components/MkKeyValue.vue"; import MkKeyValue from "@/components/MkKeyValue.vue";
import * as os from "@/os"; import * as os from "@/os";
import { login } from "@/account"; import { login } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { $i } from "@/account";
export default defineComponent({ const props = defineProps<{
components: { token: string;
XForm, }>();
MkSignin, const state = ref("");
MkKeyValue, const session = ref();
}, const fetching = ref(true);
props: ["token"], const auth_code = ref("");
data() {
return {
state: null,
session: null,
fetching: true,
i18n,
auth_code: null,
};
},
mounted() {
if (!this.$i) return;
// Fetch session onMounted(() => {
os.api("auth/session/show", { if (!$i) return;
token: this.token,
})
.then((session) => {
this.session = session;
this.fetching = false;
// os.api("auth/session/show", { token: props.token })
if (this.session.app.isAuthorized) { .then((sess: any) => {
os.api("auth/accept", { session.value = sess;
token: this.session.token, fetching.value = false;
}).then(() => {
this.accepted(); if (session.value.app.isAuthorized) {
}); os.api("auth/accept", { token: session.value.token }).then(
} else { () => {
this.state = "waiting"; accepted();
} },
}) );
.catch((error) => { } else {
this.state = "fetch-session-error"; state.value = "waiting";
this.fetching = false;
});
},
methods: {
accepted() {
this.state = "accepted";
const getUrlParams = () =>
window.location.search
.substring(1)
.split("&")
.reduce((result, query) => {
const [k, v] = query.split("=");
result[k] = decodeURI(v);
return result;
}, {});
const isMastodon = !!getUrlParams().mastodon;
if (this.session.app.callbackUrl && isMastodon) {
const callbackUrl = new URL(this.session.app.callbackUrl);
callbackUrl.searchParams.append("code", this.session.token);
if (getUrlParams().state)
callbackUrl.searchParams.append(
"state",
getUrlParams().state,
);
location.href = callbackUrl.toString();
} else if (this.session.app.callbackUrl) {
const url = new URL(this.session.app.callbackUrl);
if (
[
"javascript:",
"file:",
"data:",
"mailto:",
"tel:",
].includes(url.protocol)
)
throw new Error("invalid url");
if (
this.session.app.callbackUrl === "urn:ietf:wg:oauth:2.0:oob"
) {
this.auth_code = this.session.token;
} else {
location.href = `${this.session.app.callbackUrl}?token=${
this.session.token
}&code=${this.session.token}&state=${
getUrlParams().state || ""
}`;
}
} }
}, })
onLogin(res) { .catch((error) => {
login(res.i); state.value = "fetch-session-error";
}, fetching.value = false;
}, });
}); });
const getUrlParams = () =>
window.location.search
.substring(1)
.split("&")
.reduce((result, query) => {
const [k, v] = query.split("=");
result[k] = decodeURI(v);
return result;
}, {});
const accepted = () => {
state.value = "accepted";
const isMastodon = !!getUrlParams().mastodon;
if (session.value.app.callbackUrl && isMastodon) {
const redirectUri = decodeURIComponent(getUrlParams().redirect_uri);
if (
!session.value.app.callbackUrl
.split("\n")
.some((p) => p === redirectUri)
) {
state.value = "fetch-session-error";
fetching.value = false;
throw new Error("Callback URI doesn't match registered app");
}
const callbackUrl = new URL(redirectUri);
callbackUrl.searchParams.append("code", session.value.token);
if (getUrlParams().state)
callbackUrl.searchParams.append("state", getUrlParams().state);
location.href = callbackUrl.toString();
} else if (session.value.app.callbackUrl) {
const url = new URL(session.value.app.callbackUrl);
if (
["javascript:", "file:", "data:", "mailto:", "tel:"].includes(
url.protocol,
)
) {
throw new Error("Invalid URL");
}
if (session.value.app.callbackUrl === "urn:ietf:wg:oauth:2.0:oob") {
auth_code.value = session.value.token;
} else {
location.href = `${session.value.app.callbackUrl}?token=${
session.value.token
}&code=${session.value.token}&state=${getUrlParams().state || ""}`;
}
}
};
const onLogin = (res) => {
login(res.i);
};
</script> </script>

View file

@ -17,6 +17,15 @@
</template> </template>
</FormSelect> </FormSelect>
<FormSelect v-model="translateLang" class="_formBlock">
<template #label>
{{ i18n.ts.languageForTranslation }}
</template>
<option v-for="x in langs" :key="x[0]" :value="x[0]">
{{ x[1] }}
</option>
</FormSelect>
<FormRadios v-model="overridedDeviceKind" class="_formBlock"> <FormRadios v-model="overridedDeviceKind" class="_formBlock">
<template #label>{{ i18n.ts.overridedDeviceKind }}</template> <template #label>{{ i18n.ts.overridedDeviceKind }}</template>
<option :value="null">{{ i18n.ts.auto }}</option> <option :value="null">{{ i18n.ts.auto }}</option>
@ -71,6 +80,9 @@
{{ i18n.ts.reflectMayTakeTime }}</template {{ i18n.ts.reflectMayTakeTime }}</template
></FormSwitch ></FormSwitch
> >
<FormSwitch v-model="detectPostLanguage" class="_formBlock">{{
i18n.ts.detectPostLanguage
}}</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock"> <FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template> <template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -266,6 +278,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
const lang = ref(localStorage.getItem("lang")); const lang = ref(localStorage.getItem("lang"));
const translateLang = ref(localStorage.getItem("translateLang"));
const fontSize = ref(localStorage.getItem("fontSize")); const fontSize = ref(localStorage.getItem("fontSize"));
const useSystemFont = ref(localStorage.getItem("useSystemFont") != null); const useSystemFont = ref(localStorage.getItem("useSystemFont") != null);
@ -357,6 +370,9 @@ const showAdminUpdates = computed(
const showTimelineReplies = computed( const showTimelineReplies = computed(
defaultStore.makeGetterSetter("showTimelineReplies"), defaultStore.makeGetterSetter("showTimelineReplies"),
); );
const detectPostLanguage = computed(
defaultStore.makeGetterSetter("detectPostLanguage"),
);
watch(swipeOnDesktop, () => { watch(swipeOnDesktop, () => {
defaultStore.set("swipeOnMobile", true); defaultStore.set("swipeOnMobile", true);
@ -367,6 +383,10 @@ watch(lang, () => {
localStorage.removeItem("locale"); localStorage.removeItem("locale");
}); });
watch(translateLang, () => {
localStorage.setItem("translateLang", translateLang.value as string);
});
watch(fontSize, () => { watch(fontSize, () => {
if (fontSize.value == null) { if (fontSize.value == null) {
localStorage.removeItem("fontSize"); localStorage.removeItem("fontSize");
@ -386,6 +406,7 @@ watch(useSystemFont, () => {
watch( watch(
[ [
lang, lang,
translateLang,
fontSize, fontSize,
useSystemFont, useSystemFont,
enableInfiniteScroll, enableInfiniteScroll,

View file

@ -52,6 +52,14 @@
i18n.ts.hideOnlineStatusDescription i18n.ts.hideOnlineStatusDescription
}}</template> }}</template>
</FormSwitch> </FormSwitch>
<FormSwitch
v-model="isIndexable"
class="_formBlock"
@update:modelValue="save()"
>
{{ i18n.ts.indexable }}
<template #caption>{{ i18n.ts.indexableDescription }}</template>
</FormSwitch>
<FormSwitch <FormSwitch
v-model="noCrawle" v-model="noCrawle"
class="_formBlock" class="_formBlock"
@ -155,6 +163,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
const isLocked = ref($i.isLocked); const isLocked = ref($i.isLocked);
const autoAcceptFollowed = ref($i.autoAcceptFollowed); const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle); const noCrawle = ref($i.noCrawle);
const isIndexable = ref($i.isIndexable);
const isExplorable = ref($i.isExplorable); const isExplorable = ref($i.isExplorable);
const hideOnlineStatus = ref($i.hideOnlineStatus); const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions); const publicReactions = ref($i.publicReactions);
@ -178,6 +187,7 @@ function save() {
isLocked: !!isLocked.value, isLocked: !!isLocked.value,
autoAcceptFollowed: !!autoAcceptFollowed.value, autoAcceptFollowed: !!autoAcceptFollowed.value,
noCrawle: !!noCrawle.value, noCrawle: !!noCrawle.value,
isIndexable: !!isIndexable.value,
isExplorable: !!isExplorable.value, isExplorable: !!isExplorable.value,
hideOnlineStatus: !!hideOnlineStatus.value, hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value, publicReactions: !!publicReactions.value,

View file

@ -237,15 +237,35 @@ export function getNoteMenu(props: {
}); });
} }
async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", {
noteId: noteId,
targetLang: targetLang,
});
}
async function translate(): Promise<void> { async function translate(): Promise<void> {
const translateLang = localStorage.getItem("translateLang");
const lang = localStorage.getItem("lang");
if (props.translation.value != null) return; if (props.translation.value != null) return;
props.translating.value = true; props.translating.value = true;
const res = await os.api("notes/translate", { props.translation.value = await translate_(
noteId: appearNote.id, appearNote.id,
targetLang: localStorage.getItem("lang") || navigator.language, translateLang || lang || navigator.language,
}); );
// use UI language as the second translation target
if (
translateLang != null &&
lang != null &&
translateLang !== lang &&
(!props.translation.value ||
props.translation.value.sourceLang.toLowerCase() ===
translateLang.slice(0, 2))
)
props.translation.value = await translate_(appearNote.id, lang);
props.translating.value = false; props.translating.value = false;
props.translation.value = res;
} }
let menu; let menu;

View file

@ -1,6 +1,5 @@
import { markRaw, ref } from "vue"; import { markRaw, ref } from "vue";
import { Storage } from "./pizzax"; import { Storage } from "./pizzax";
import { Theme } from "./scripts/theme";
export const postFormActions = []; export const postFormActions = [];
export const userActions = []; export const userActions = [];
@ -346,6 +345,10 @@ export const defaultStore = markRaw(
where: "account", where: "account",
default: true, default: true,
}, },
detectPostLanguage: {
where: "deviceAccount",
default: true,
},
}), }),
); );

View file

@ -57,6 +57,14 @@
><i class="ph-image-square ph-bold ph-lg icon"></i ><i class="ph-image-square ph-bold ph-lg icon"></i
>{{ i18n.ts.gallery }}</MkA >{{ i18n.ts.gallery }}</MkA
> >
<button
class="_button link"
active-class="active"
@click="search()"
>
<i class="ph-magnifying-glass ph-bold ph-lg icon"></i
><span>{{ i18n.ts.search }}</span>
</button>
<div class="action"> <div class="action">
<button class="_buttonPrimary" @click="signup()"> <button class="_buttonPrimary" @click="signup()">
{{ i18n.ts.signup }} {{ i18n.ts.signup }}

File diff suppressed because it is too large Load diff