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
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Worker only mode
#onlyQueueProcessor: 1
# Number of worker processes by type.
# The sum must not exceed the number of available cores.
#clusterLimits:
# web: 1
# queue: 1
# Job concurrency per worker
# deliverJobConcurrency: 128

View file

@ -3,7 +3,13 @@
**What does this PR do?** _(Please give us a brief description of what this PR does.)_
**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 have made sure to test 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
## [1.0.4-beta] - 2023-08-02
## [1.0.4-beta2] - 2023-09-02
### Bug Fixes
@ -16,8 +16,6 @@
- Fix: :bug: make admin users page properly direct user cards to about page
- Fix?
- Fix: :globe_with_meridians: copying origin: "remote" -> "origin"
- 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 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
@ -149,6 +196,12 @@ ref: https://frfsh.plus.st/notes/9hqswpwiwjaihcgo
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
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
@ -758,11 +811,491 @@ Translate-URL: https://hosted.weblate.org/projects/firefish/locales/de/
- Chore: Merge branch 'origin/develop' into Weblate.
- 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
- Perf: :zap: featured posts query limit
- Perf: :zap: delete transformedOptions key -> assign undefined with key literal
- Perf: :zap: seperate web and queue workers
### 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: :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
@ -797,6 +1350,20 @@ ref: https://git.joinfirefish.org/firefish/firefish/-/issues/10527#note_230
### 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)
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: :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.
@ -901,22 +1468,6 @@ Adjusts ratelimit to 250 files every 10 minutes, fixes error text, fixes reused
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
@ -8900,7 +9451,7 @@ Resolve #7540
* truncate user information if it is too 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.
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
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
@ -14921,7 +15472,7 @@ Defaults for `local` and `withFiles` are based on the behaviour of the endpoint.
* 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
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`
@ -15551,7 +16102,7 @@ unnecessarily loaded.
* remove duplicate null check
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`

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.
## 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.
PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
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.
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.
## Well-known branches
- The **`main`** branch is tracking the latest release and used for production purposes.
- 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.
- **`feature/*`** branches are reserved for the development of a specific feature
## Creating a PR
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, 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.
- 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.
- 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`
## Creating a merge request (MR)
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 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 MR is rejected.
- 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 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.
- 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.
- 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)
- 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 🤗
@ -56,12 +57,12 @@ Be willing to comment on the good points and not just the things you want fixed
### Review perspective
- Scope
- Are the goals of the PR clear?
- Is the granularity of the PR appropriate?
- Are the goals of the MR clear?
- Is the granularity of the MR appropriate?
- Security
- Does merging this PR create a vulnerability?
- Does merging this MR create a vulnerability?
- Performance
- Will merging this PR cause unexpected performance degradation?
- Will merging this MR cause unexpected performance degradation?
- Is there a more efficient way?
- Testing
- 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?
## 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>
```
An actual domain will be assigned so you can test the federation.
# THE FOLLOWING IS OUTDATED:
## Merge
## 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.
In addition, it will also automatically start the Misskey server process.
# THE FOLLOWING IS OUTDATED:
## Testing
- 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 sounds, place only mp3 files in the `./custom/assets/sounds` directory.
- 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

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"
driveUsage: "Drive space usage"
noCrawle: "Reject crawler indexing"
noCrawleDescription: "Ask search engines to not index your profile page, posts, Pages,
etc."
noCrawleDescription: "Ask external search engines to not index your content."
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."
alwaysMarkSensitive: "Mark as NSFW by default"
@ -1139,6 +1138,10 @@ confirm: "Confirm"
importZip: "Import ZIP"
exportZip: "Export ZIP"
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:
description: "Reduces the effort of server moderation through automatically recognizing

View file

@ -874,7 +874,7 @@ pubSub: "Cuentas Pub/Sub"
lastCommunication: "Última comunicación"
resolved: "Resuelto"
unresolved: "Sin resolver"
breakFollow: "Dejar de seguir"
breakFollow: "Quitar seguidor"
itsOn: "¡Está encendido!"
itsOff: "¡Está apagado!"
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"
unableToDelete: "Suppression impossible"
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"
circularReferenceFolder: "Le dossier de destination est un sous-dossier du dossier
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"
rename: "Renommer"
avatar: "Avatar"
@ -605,7 +605,7 @@ disablePlayer: "Fermer le lecteur vidéo"
expandTweet: "Étendre le tweet"
themeEditor: "Éditeur de thèmes"
description: "Description"
describeFile: "Ajouter une description d'image"
describeFile: "Ajouter une description"
enterFileDescription: "Saisissez une description"
author: "Auteur·rice"
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer
@ -2085,7 +2085,7 @@ silenceThisInstance: Masquer ce serveur
silencedInstances: Serveurs masqués
silenced: Masqué
deleted: Effacé
editNote: Modifier publication
editNote: Modifier la publication
edited: 'Modifié à {date} {time}'
flagShowTimelineRepliesDescription: Si activé, affiche dans le fil les réponses des
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
importZip: Importer 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"
addAccount: "Aggiungi account"
loginFailed: "Accesso non riuscito"
showOnRemote: "Apri la pagina di origine"
showOnRemote: "Visita la pagina di origine"
general: "Generali"
wallpaper: "Sfondo"
setWallpaper: "Imposta sfondo"

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "firefish",
"version": "1.0.5-dev8",
"version": "1.0.5-dev11",
"codename": "aqua",
"repository": {
"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 m20230709_000510_move_antenna_to_cache;
mod m20230806_170616_fix_antenna_stream_ids;
mod m20230904_013244_is_indexable;
pub struct Migrator;
@ -17,6 +18,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230627_185451_index_note_url::Migration),
Box::new(m20230709_000510_move_antenna_to_cache::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>,
#[sea_orm(column_name = "speakAsCat")]
pub speak_as_cat: bool,
#[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

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

View file

@ -19,7 +19,12 @@ const ev = new Xev();
* Init process
*/
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) {
await masterMain();

View file

@ -111,7 +111,7 @@ export async function masterMain() {
bootLogger.succ("Firefish initialized");
if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit);
await spawnWorkers(config.clusterLimits);
}
bootLogger.succ(
@ -120,7 +120,11 @@ export async function masterMain() {
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/queue-stats.js").then((x) => x.default());
import("../daemons/janitor.js").then((x) => x.default());
@ -136,7 +140,7 @@ function showEnvironment(): void {
if (env !== "production") {
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) {
const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? "" : "s"}...`);
await Promise.all([...Array(workers)].map(spawnWorker));
async function spawnWorkers(
clusterLimits: Required<Config["clusterLimits"]>,
): Promise<void> {
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");
}
function spawnWorker(): Promise<void> {
function spawnWorker(mode: "web" | "queue"): Promise<void> {
return new Promise((res) => {
const worker = cluster.fork();
const worker = cluster.fork({ mode });
worker.on("message", (message) => {
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);
}
if (message !== "ready") return;

View file

@ -1,6 +1,7 @@
import cluster from "node:cluster";
import { initDb } from "../db/postgre.js";
import config from "@/config/index.js";
import os from "node:os";
/**
* Init worker process
@ -8,13 +9,20 @@ import config from "@/config/index.js";
export async function workerMain() {
await initDb();
if (!config.onlyQueueProcessor) {
if (!process.env.mode || process.env.mode === "web") {
// start server
await import("../server/index.js").then((x) => x.default());
}
// start job queue
import("../queue/index.js").then((x) => x.default());
if (!process.env.mode || process.env.mode === "queue") {
// 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) {
// Send a 'ready' message to parent process

View file

@ -59,6 +59,23 @@ export default function load() {
if (config.cacheServer && !config.cacheServer.prefix)
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);
}

View file

@ -77,9 +77,10 @@ export type Source = {
accesslog?: string;
clusterLimit?: number;
onlyQueueProcessor?: boolean;
clusterLimits?: {
web?: number;
queue?: number;
};
cuid?: {
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;

View file

@ -12,29 +12,38 @@ const retryDelay = 100;
* @param timeout Lock timeout (ms), The timeout releases previous lock.
* @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}`, {
lockTimeout: timeout,
retryInterval: retryDelay,
});
await lock.acquire();
return lock;
}
export async function getFetchInstanceMetadataLock(
host: string,
timeout = 30 * 1000,
) {
): Promise<Mutex> {
const lock = new Mutex(redisClient, `instance:${host}`, {
lockTimeout: timeout,
retryInterval: retryDelay,
});
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}`, {
lockTimeout: timeout,
retryInterval: retryDelay,
});
await lock.acquire();
return lock;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -454,6 +454,8 @@ export async function resolveNote(
`host ${extractDbHost(uri)} is blocked`,
);
const lock = await getApLock(uri);
try {
//#region Returns if already registered with this server
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.
return await createNote(uri, resolver, true);
} finally {
await getApLock(uri);
await lock.release();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -764,7 +764,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchNote);
}
if (publishing) {
if (publishing && user.isIndexable) {
index(note, true);
// 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 { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Notes } from "@/models/index.js";
import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js";
import define from "../../define.js";
@ -12,7 +11,7 @@ import define from "../../define.js";
export const meta = {
tags: ["notes"],
requireCredential: false,
requireCredential: true,
requireCredentialPrivateMode: true,
res: {

View file

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

View file

@ -24,9 +24,11 @@ block meta
unless privateMode
if profile.noCrawle
meta(name='robots' content='noindex')
if profile.preventAiLearning
meta(name='robots' content='noai')
meta(name='robots' content='noimageai')
meta(name='GPTBot' content='noindex')
meta(name='misskey:user-username' content=user.username)
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}`;
const lock = await getChartInsertLock(lockKey);
try {
// ロック内でもう1回チェックする
const currentLog = (await repository.findOneBy({
@ -465,14 +466,14 @@ export default abstract class Chart<T extends Schema> {
return log;
} finally {
await getChartInsertLock(lockKey);
await lock.release();
}
}
protected commit(diff: Commit<T>, group: string | null = null): void {
for (const [k, v] of Object.entries(diff)) {
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];
}
this.buffer.push({

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -111,7 +111,7 @@
></MkSubNoteContent>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini />
<div v-else class="translated">
<div v-else-if="translation != null" class="translated">
<b
>{{
i18n.t("translatedFrom", {
@ -219,6 +219,18 @@
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<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
ref="menuButton"
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 type { Ref } from "vue";
import type * as misskey from "firefish-js";
import { detect as detectLanguage_ } from "tinyld";
import MkSubNoteContent from "./MkSubNoteContent.vue";
import MkNoteSub from "@/components/MkNoteSub.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue";
@ -346,6 +359,57 @@ const translation = ref(null);
const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
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 = {
r: () => reply(true),

View file

@ -124,6 +124,18 @@
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<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
ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more"
@ -180,6 +192,8 @@
import { computed, inject, ref } from "vue";
import type { Ref } from "vue";
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 MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
@ -266,6 +280,57 @@ const replies: misskey.entities.Note[] =
.reverse() ?? [];
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
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({
rootEl: el,

View file

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

View file

@ -45,108 +45,97 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import XForm from "./auth.form.vue";
import MkSignin from "@/components/MkSignin.vue";
import MkKeyValue from "@/components/MkKeyValue.vue";
import * as os from "@/os";
import { login } from "@/account";
import { i18n } from "@/i18n";
import { $i } from "@/account";
export default defineComponent({
components: {
XForm,
MkSignin,
MkKeyValue,
},
props: ["token"],
data() {
return {
state: null,
session: null,
fetching: true,
i18n,
auth_code: null,
};
},
mounted() {
if (!this.$i) return;
const props = defineProps<{
token: string;
}>();
const state = ref("");
const session = ref();
const fetching = ref(true);
const auth_code = ref("");
// Fetch session
os.api("auth/session/show", {
token: this.token,
})
.then((session) => {
this.session = session;
this.fetching = false;
onMounted(() => {
if (!$i) return;
//
if (this.session.app.isAuthorized) {
os.api("auth/accept", {
token: this.session.token,
}).then(() => {
this.accepted();
});
} else {
this.state = "waiting";
}
})
.catch((error) => {
this.state = "fetch-session-error";
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 || ""
}`;
}
os.api("auth/session/show", { token: props.token })
.then((sess: any) => {
session.value = sess;
fetching.value = false;
if (session.value.app.isAuthorized) {
os.api("auth/accept", { token: session.value.token }).then(
() => {
accepted();
},
);
} else {
state.value = "waiting";
}
},
onLogin(res) {
login(res.i);
},
},
})
.catch((error) => {
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>

View file

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

View file

@ -52,6 +52,14 @@
i18n.ts.hideOnlineStatusDescription
}}</template>
</FormSwitch>
<FormSwitch
v-model="isIndexable"
class="_formBlock"
@update:modelValue="save()"
>
{{ i18n.ts.indexable }}
<template #caption>{{ i18n.ts.indexableDescription }}</template>
</FormSwitch>
<FormSwitch
v-model="noCrawle"
class="_formBlock"
@ -155,6 +163,7 @@ import { definePageMetadata } from "@/scripts/page-metadata";
const isLocked = ref($i.isLocked);
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle);
const isIndexable = ref($i.isIndexable);
const isExplorable = ref($i.isExplorable);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
@ -178,6 +187,7 @@ function save() {
isLocked: !!isLocked.value,
autoAcceptFollowed: !!autoAcceptFollowed.value,
noCrawle: !!noCrawle.value,
isIndexable: !!isIndexable.value,
isExplorable: !!isExplorable.value,
hideOnlineStatus: !!hideOnlineStatus.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> {
const translateLang = localStorage.getItem("translateLang");
const lang = localStorage.getItem("lang");
if (props.translation.value != null) return;
props.translating.value = true;
const res = await os.api("notes/translate", {
noteId: appearNote.id,
targetLang: localStorage.getItem("lang") || navigator.language,
});
props.translation.value = await translate_(
appearNote.id,
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.translation.value = res;
}
let menu;

View file

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

View file

@ -57,6 +57,14 @@
><i class="ph-image-square ph-bold ph-lg icon"></i
>{{ 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">
<button class="_buttonPrimary" @click="signup()">
{{ i18n.ts.signup }}

File diff suppressed because it is too large Load diff