Merge remote-tracking branch 'origin/develop' into develop
# Conflicts: # CALCKEY.md # package.json # push-docker.sh
This commit is contained in:
commit
ad38dd201b
179 changed files with 1376 additions and 477 deletions
|
@ -93,6 +93,9 @@ id: 'aid'
|
||||||
# ┌─────────────────────┐
|
# ┌─────────────────────┐
|
||||||
#───┘ Other configuration └─────────────────────────────────────
|
#───┘ Other configuration └─────────────────────────────────────
|
||||||
|
|
||||||
|
# Max note length, should be < 8000.
|
||||||
|
#maxNoteLength: 3000
|
||||||
|
|
||||||
# Whether disable HSTS
|
# Whether disable HSTS
|
||||||
#disableHsts: true
|
#disableHsts: true
|
||||||
|
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -59,3 +59,8 @@ packages/backend/assets/instance.css
|
||||||
*.blend3
|
*.blend3
|
||||||
*.blend4
|
*.blend4
|
||||||
*.blend5
|
*.blend5
|
||||||
|
|
||||||
|
#intelij stuff
|
||||||
|
packages/backend/.idea/backend.iml
|
||||||
|
packages/backend/.idea/modules.xml
|
||||||
|
packages/backend/.idea/vcs.xml
|
||||||
|
|
53
CALCKEY.md
53
CALCKEY.md
|
@ -2,34 +2,41 @@
|
||||||
|
|
||||||
## Planned
|
## Planned
|
||||||
|
|
||||||
- MFM button
|
- Stucture
|
||||||
- Classic mode make instance icon bring up new context menu
|
- [Sonic](https://crates.io/crates/sonic-server) support as an ElasticSearch alternative
|
||||||
- Exclude self from antenna
|
- [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative
|
||||||
- Backfill remote users
|
- Optionally use [ScyllaDB](https://www.scylladb.com/open-source-nosql-database/) for storing notes
|
||||||
- User "choices" (recommended users) like Mastodon and Soapbox
|
- Rewrite backend in Rust and [Axum](https://github.com/tokio-rs/axum)
|
||||||
- Option to publicize instance blocks
|
- Function
|
||||||
- Fully revamp non-logged-in screen
|
- Federate with note edits
|
||||||
- Personal notes for all accounts
|
- Admin customizable max note length (100-8000)
|
||||||
- Non-nyaify cat mode
|
- User "choices" (recommended users) like Mastodon and Soapbox
|
||||||
- Timeline filters
|
- Join Reason system like Mastodon/Pleroma
|
||||||
- Filter notifications by user
|
- Option to publicize instance blocks
|
||||||
- Join Reason system like Mastodon/Pleroma
|
- Backfill remote users
|
||||||
- Build flag to remove NSFW/AI stuff
|
- Build flag to remove NSFW/AI stuff
|
||||||
- [Rat mode?](https://stop.voring.me/notes/933fx97bmd)
|
- Timeline filters
|
||||||
|
- Filter notifications by user
|
||||||
|
- Non-nyaify cat mode
|
||||||
|
- Exclude self from antenna
|
||||||
|
- Form
|
||||||
|
- MFM button
|
||||||
|
- Personal notes for all accounts
|
||||||
|
- Fully revamp non-logged-in screen
|
||||||
|
- Classic mode make instance icon bring up new context menu
|
||||||
|
- [Rat mode?](https://stop.voring.me/notes/933fx97bmd)
|
||||||
|
|
||||||
## Work in progress
|
## Work in progress
|
||||||
|
|
||||||
|
- Weblate project
|
||||||
|
- Customizable max note length
|
||||||
|
- Link verification
|
||||||
- Better Messaging UI
|
- Better Messaging UI
|
||||||
- Better API Documentation
|
- Better API Documentation
|
||||||
- Remote follow button
|
- Remote follow button
|
||||||
- Admin custom CSS
|
- Admin custom CSS
|
||||||
- Add back time machine (jump to date)
|
- Add back time machine (jump to date)
|
||||||
- Improve accesibility score
|
- Improve accesibility
|
||||||
<details><summary>Current Misskey score is 57/100</summary>
|
|
||||||
|
|
||||||
![accesibility score](https://pool.jortage.com/voringme/misskey/8ff18aae-4dc6-4b08-9e05-a4c9d051a9e3.png)
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
|
@ -38,6 +45,7 @@
|
||||||
- Fix Dockerfile @hanna
|
- Fix Dockerfile @hanna
|
||||||
- Upgrade packages with security vunrabilities
|
- Upgrade packages with security vunrabilities
|
||||||
- Saner defaults
|
- Saner defaults
|
||||||
|
- Fediverse account migration
|
||||||
- Recommended instances timeline
|
- Recommended instances timeline
|
||||||
- OCR image captioning
|
- OCR image captioning
|
||||||
- Improve mobile UX
|
- Improve mobile UX
|
||||||
|
@ -88,8 +96,10 @@
|
||||||
- Page drafts
|
- Page drafts
|
||||||
- Patron list
|
- Patron list
|
||||||
- Animations respect reduced motion
|
- Animations respect reduced motion
|
||||||
- Obliteration of Ai-chan
|
|
||||||
- Undo renote button inside original note
|
- Undo renote button inside original note
|
||||||
|
- Custom locales
|
||||||
|
- Obliteration of Ai-chan
|
||||||
|
- Switch to [Calckey.js](https://codeberg.org/thatonecalculator/calckey.js)
|
||||||
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
||||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
||||||
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
||||||
|
@ -131,3 +141,4 @@
|
||||||
- 4c5aa9e53887cca5561fcec6ab0754e018f589a5: server: allow to like own pages
|
- 4c5aa9e53887cca5561fcec6ab0754e018f589a5: server: allow to like own pages
|
||||||
- 923c93da1228458dd65be47483c198a1a9191bcf: use await for notes.countBy
|
- 923c93da1228458dd65be47483c198a1a9191bcf: use await for notes.countBy
|
||||||
- ca90cedba0a0704b503c2778694230f5a7dfbace: server: reduce dead instance detection to 7 days
|
- ca90cedba0a0704b503c2778694230f5a7dfbace: server: reduce dead instance detection to 7 days
|
||||||
|
- e9ab42c10afb4e27516c2d2b5e3e06630efe9edd: Alt text in image viewer
|
||||||
|
|
|
@ -1,46 +1,136 @@
|
||||||
|
|
||||||
# Contributor Covenant Code of Conduct
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
## Our Standards
|
## Our Standards
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment include:
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
* Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing viewpoints and experiences
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Gracefully accepting constructive criticism
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Focusing on what is best for the community
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
* Showing empathy towards other community members
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
* Publishing others' private information, such as a physical or email address,
|
||||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at syuilotan@yahoo.co.jp. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
@thatonecalculator on Codeberg,
|
||||||
|
`@thatonecalculator@stop.voring.me` or `@t1c@i.calckey.cloud` on the Fediverse,
|
||||||
|
or kainoa@t1c.dev via email.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ COPY . ./
|
||||||
|
|
||||||
# Install Dependencies
|
# Install Dependencies
|
||||||
RUN apk update
|
RUN apk update
|
||||||
RUN apk add git ffmpeg tini alpine-sdk
|
RUN apk add git ffmpeg tini alpine-sdk python3
|
||||||
|
|
||||||
# Configure corepack and yarn
|
# Configure corepack and yarn
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
- Notable differences:
|
- Notable differences:
|
||||||
- Improved UI/UX (especially on mobile)
|
- Improved UI/UX (especially on mobile)
|
||||||
- Improved notifications
|
- Improved notifications
|
||||||
|
- Fediverse account migration
|
||||||
- Improved instance security
|
- Improved instance security
|
||||||
- Improved accessibility
|
- Improved accessibility
|
||||||
- Recommended Instances timeline
|
- Recommended Instances timeline
|
||||||
|
@ -49,10 +50,10 @@ This guide will work for both **starting from scratch** and **migrating from Mis
|
||||||
|
|
||||||
## 📦 Dependencies
|
## 📦 Dependencies
|
||||||
|
|
||||||
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19.1.0 recommended)
|
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19 recommended)
|
||||||
- Install with [nvm](https://github.com/nvm-sh/nvm)
|
- Install with [nvm](https://github.com/nvm-sh/nvm)
|
||||||
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
|
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
|
||||||
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
|
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommend)
|
||||||
|
|
||||||
### 😗 Optional dependencies
|
### 😗 Optional dependencies
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ services:
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
restart: always
|
restart: always
|
||||||
image: docker.io/redis:4.0-alpine
|
image: docker.io/redis:7.0-alpine
|
||||||
networks:
|
networks:
|
||||||
- network
|
- network
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -112,6 +112,7 @@ reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drück
|
||||||
rememberNoteVisibility: "Notizsichtbarkeit merken"
|
rememberNoteVisibility: "Notizsichtbarkeit merken"
|
||||||
attachCancel: "Anhang entfernen"
|
attachCancel: "Anhang entfernen"
|
||||||
markAsSensitive: "Als NSFW markieren"
|
markAsSensitive: "Als NSFW markieren"
|
||||||
|
accountMoved: "Benutzer hat zu einem anderen Account gewechselt."
|
||||||
unmarkAsSensitive: "Als nicht NSFW markieren"
|
unmarkAsSensitive: "Als nicht NSFW markieren"
|
||||||
enterFileName: "Dateinamen eingeben"
|
enterFileName: "Dateinamen eingeben"
|
||||||
mute: "Stummschalten"
|
mute: "Stummschalten"
|
||||||
|
|
|
@ -59,7 +59,7 @@ followRequestAccepted: "Follow request accepted"
|
||||||
mention: "Mention"
|
mention: "Mention"
|
||||||
mentions: "Mentions"
|
mentions: "Mentions"
|
||||||
directNotes: "Direct notes"
|
directNotes: "Direct notes"
|
||||||
importAndExport: "Import / Export"
|
importAndExport: "Import/Export Data"
|
||||||
import: "Import"
|
import: "Import"
|
||||||
export: "Export"
|
export: "Export"
|
||||||
files: "Files"
|
files: "Files"
|
||||||
|
@ -149,6 +149,7 @@ addAccount: "Add account"
|
||||||
loginFailed: "Failed to sign in"
|
loginFailed: "Failed to sign in"
|
||||||
showOnRemote: "View on remote instance"
|
showOnRemote: "View on remote instance"
|
||||||
general: "General"
|
general: "General"
|
||||||
|
accountMoved: "User has moved to a new account:"
|
||||||
wallpaper: "Wallpaper"
|
wallpaper: "Wallpaper"
|
||||||
setWallpaper: "Set wallpaper"
|
setWallpaper: "Set wallpaper"
|
||||||
removeWallpaper: "Remove wallpaper"
|
removeWallpaper: "Remove wallpaper"
|
||||||
|
@ -920,6 +921,15 @@ swipeOnDesktop: "Allow mobile-style swiping on desktop"
|
||||||
logoImageUrl: "Logo image URL"
|
logoImageUrl: "Logo image URL"
|
||||||
showAdminUpdates: "Indicate a new Calckey version is avaliable (admin only)"
|
showAdminUpdates: "Indicate a new Calckey version is avaliable (admin only)"
|
||||||
replayTutorial: "Replay tutorial"
|
replayTutorial: "Replay tutorial"
|
||||||
|
migration: "Migration"
|
||||||
|
moveTo: "Move current account to new account"
|
||||||
|
moveToLabel: "Account you're moving to:"
|
||||||
|
moveAccount: "Move account!"
|
||||||
|
moveAccountDescription: "This process is irriversable. Make sure you've set up an alias for this account on your new account before moving. Please enter the tag of the account formatted like @person@instance.com"
|
||||||
|
moveFrom: "Move to this account from an older account"
|
||||||
|
moveFromLabel: "Account you're moving from:"
|
||||||
|
moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com"
|
||||||
|
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from."
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||||
|
|
|
@ -149,6 +149,7 @@ addAccount: "アカウントを追加"
|
||||||
loginFailed: "ログインに失敗しました"
|
loginFailed: "ログインに失敗しました"
|
||||||
showOnRemote: "リモートで表示"
|
showOnRemote: "リモートで表示"
|
||||||
general: "全般"
|
general: "全般"
|
||||||
|
accountMoved: "このユーザーは新しいアカウントに移行しました"
|
||||||
wallpaper: "壁紙"
|
wallpaper: "壁紙"
|
||||||
setWallpaper: "壁紙を設定"
|
setWallpaper: "壁紙を設定"
|
||||||
removeWallpaper: "壁紙を削除"
|
removeWallpaper: "壁紙を削除"
|
||||||
|
|
|
@ -108,7 +108,7 @@ sensitive: "열람주의"
|
||||||
add: "추가"
|
add: "추가"
|
||||||
reaction: "리액션"
|
reaction: "리액션"
|
||||||
reactionSetting: "선택기에 표시할 리액션"
|
reactionSetting: "선택기에 표시할 리액션"
|
||||||
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
|
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
|
||||||
rememberNoteVisibility: "공개 범위를 기억하기"
|
rememberNoteVisibility: "공개 범위를 기억하기"
|
||||||
attachCancel: "첨부 취소"
|
attachCancel: "첨부 취소"
|
||||||
markAsSensitive: "열람주의로 설정"
|
markAsSensitive: "열람주의로 설정"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "12.119.0-calc.18",
|
"version": "13.0.0-beta6",
|
||||||
"codename": "aqua",
|
"codename": "aqua",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
"start:test": "yarn workspace backend run start:test",
|
"start:test": "yarn workspace backend run start:test",
|
||||||
"init": "yarn migrate",
|
"init": "yarn migrate",
|
||||||
"migrate": "yarn workspace backend run migrate",
|
"migrate": "yarn workspace backend run migrate",
|
||||||
|
"revertmigration": "yarn workspace backend run revertmigration",
|
||||||
"migrateandstart": "yarn migrate && yarn start",
|
"migrateandstart": "yarn migrate && yarn start",
|
||||||
"gulp": "gulp build",
|
"gulp": "gulp build",
|
||||||
"watch": "yarn dev",
|
"watch": "yarn dev",
|
||||||
|
@ -42,6 +43,7 @@
|
||||||
"@bull-board/api": "^4.6.4",
|
"@bull-board/api": "^4.6.4",
|
||||||
"@bull-board/ui": "^4.6.4",
|
"@bull-board/ui": "^4.6.4",
|
||||||
"@tensorflow/tfjs": "^3.21.0",
|
"@tensorflow/tfjs": "^3.21.0",
|
||||||
|
"calckey-js": "^0.0.17",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"gulp": "4.0.2",
|
"gulp": "4.0.2",
|
||||||
|
|
8
packages/backend/.idea/.gitignore
vendored
Normal file
8
packages/backend/.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
|
@ -0,0 +1,16 @@
|
||||||
|
export class addMovedToAndKnownAs1669288094000 {
|
||||||
|
name = 'addMovedToAndKnownAs1669288094000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "movedToUri" character varying(512)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "alsoKnownAs" TEXT`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedToUri"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAs"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
"start": "node ./built/index.js",
|
"start": "node ./built/index.js",
|
||||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
"start:test": "NODE_ENV=test node ./built/index.js",
|
||||||
"migrate": "typeorm migration:run -d ormconfig.js",
|
"migrate": "typeorm migration:run -d ormconfig.js",
|
||||||
|
"revertmigration": "typeorm migration:revert -d ormconfig.js",
|
||||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||||
"watch": "node watch.mjs",
|
"watch": "node watch.mjs",
|
||||||
"lint": "eslint --quiet \"src/**/*.ts\"",
|
"lint": "eslint --quiet \"src/**/*.ts\"",
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"bull": "4.10.1",
|
"bull": "4.10.1",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
|
"calckey-js": "^0.0.17",
|
||||||
"cbor": "8.1.0",
|
"cbor": "8.1.0",
|
||||||
"chalk": "5.1.2",
|
"chalk": "5.1.2",
|
||||||
"chalk-template": "0.4.0",
|
"chalk-template": "0.4.0",
|
||||||
|
@ -76,7 +78,6 @@
|
||||||
"koa-views": "7.0.2",
|
"koa-views": "7.0.2",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.0",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "0.0.14",
|
|
||||||
"mocha": "10.1.0",
|
"mocha": "10.1.0",
|
||||||
"multer": "1.4.4-lts.1",
|
"multer": "1.4.4-lts.1",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
|
|
|
@ -67,6 +67,7 @@ export type Source = {
|
||||||
// Managed hosting stuff
|
// Managed hosting stuff
|
||||||
maxUserSignups?: number;
|
maxUserSignups?: number;
|
||||||
isManagedHosting?: boolean;
|
isManagedHosting?: boolean;
|
||||||
|
maxNoteLength?: number;
|
||||||
deepl: {
|
deepl: {
|
||||||
managed?: boolean;
|
managed?: boolean;
|
||||||
authKey?: string;
|
authKey?: string;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export const MAX_NOTE_TEXT_LENGTH = 3000;
|
import config from '@/config/index.js';
|
||||||
|
|
||||||
|
export const MAX_NOTE_TEXT_LENGTH = config.maxNoteLength != null ? config.maxNoteLength : 3000;
|
||||||
|
|
||||||
export const SECOND = 1000;
|
export const SECOND = 1000;
|
||||||
export const SEC = 1000;
|
export const SEC = 1000;
|
||||||
|
|
|
@ -7,7 +7,7 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||||
if (!noCache && cache) return cache;
|
if (!noCache && cache) return cache;
|
||||||
|
|
||||||
return await db.transaction(async transactionalEntityManager => {
|
return await db.transaction(async transactionalEntityManager => {
|
||||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
// New IDs are prioritized because multiple records may have been created due to past bugs.
|
||||||
const metas = await transactionalEntityManager.find(Meta, {
|
const metas = await transactionalEntityManager.find(Meta, {
|
||||||
order: {
|
order: {
|
||||||
id: 'DESC',
|
id: 'DESC',
|
||||||
|
@ -20,7 +20,7 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
|
||||||
cache = meta;
|
cache = meta;
|
||||||
return meta;
|
return meta;
|
||||||
} else {
|
} else {
|
||||||
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
|
// If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert.
|
||||||
const saved = await transactionalEntityManager
|
const saved = await transactionalEntityManager
|
||||||
.upsert(
|
.upsert(
|
||||||
Meta,
|
Meta,
|
||||||
|
|
|
@ -33,7 +33,6 @@ export async function skippedInstances(hosts: Array<Instace['host']>): Array<Ins
|
||||||
})
|
})
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where('instance.isSuspended')
|
.where('instance.isSuspended')
|
||||||
.orWhere('instance.lastCommunicatedAt < :deadTime', { deadTime });
|
|
||||||
}))
|
}))
|
||||||
.select('host')
|
.select('host')
|
||||||
.getRawMany()
|
.getRawMany()
|
||||||
|
|
|
@ -68,6 +68,19 @@ export class User {
|
||||||
})
|
})
|
||||||
public followingCount: number;
|
public followingCount: number;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 512,
|
||||||
|
nullable: true,
|
||||||
|
comment: 'The URI of the new account of the User',
|
||||||
|
})
|
||||||
|
public movedToUri: string | null;
|
||||||
|
|
||||||
|
@Column('simple-array', {
|
||||||
|
nullable: true,
|
||||||
|
comment: 'URIs the user is known as too',
|
||||||
|
})
|
||||||
|
public alsoKnownAs: string[] | null;
|
||||||
|
|
||||||
@Column('integer', {
|
@Column('integer', {
|
||||||
default: 0,
|
default: 0,
|
||||||
comment: 'The count of notes.',
|
comment: 'The count of notes.',
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { PageLike } from '@/models/entities/page-like.js';
|
import { PageLike } from '@/models/entities/page-like.js';
|
||||||
|
import type { User } from '@/models/entities/user.js';
|
||||||
import { Pages } from '../index.js';
|
import { Pages } from '../index.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
|
||||||
|
|
||||||
export const PageLikeRepository = db.getRepository(PageLike).extend({
|
export const PageLikeRepository = db.getRepository(PageLike).extend({
|
||||||
async pack(
|
async pack(
|
||||||
src: PageLike['id'] | PageLike,
|
src: PageLike['id'] | PageLike,
|
||||||
me?: { id: User['id'] } | null | undefined
|
me?: { id: User['id'] } | null | undefined,
|
||||||
) {
|
) {
|
||||||
const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ export const PageLikeRepository = db.getRepository(PageLike).extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
packMany(
|
packMany(
|
||||||
likes: any[],
|
likes: PageLike[],
|
||||||
me: { id: User['id'] }
|
me: { id: User['id'] },
|
||||||
) {
|
) {
|
||||||
return Promise.all(likes.map(x => this.pack(x, me)));
|
return Promise.all(likes.map(x => this.pack(x, me)));
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { Page } from '@/models/entities/page.js';
|
import { Page } from '@/models/entities/page.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import type { Packed } from '@/misc/schema.js';
|
||||||
import { awaitAll } from '@/prelude/await-all.js';
|
import { awaitAll } from '@/prelude/await-all.js';
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
import type { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import type { User } from '@/models/entities/user.js';
|
||||||
import { Users, DriveFiles, PageLikes } from '../index.js';
|
import { Users, DriveFiles, PageLikes } from '../index.js';
|
||||||
|
|
||||||
export const PageRepository = db.getRepository(Page).extend({
|
export const PageRepository = db.getRepository(Page).extend({
|
||||||
|
|
|
@ -1,16 +1,43 @@
|
||||||
import { EntityRepository, Repository, In, Not } from 'typeorm';
|
import { URL } from 'url';
|
||||||
|
import { In, Not } from 'typeorm';
|
||||||
import Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
import type { ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||||
|
import { User } from '@/models/entities/user.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import type { Packed } from '@/misc/schema.js';
|
||||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
import type { Promiseable } from '@/prelude/await-all.js';
|
||||||
|
import { awaitAll } from '@/prelude/await-all.js';
|
||||||
import { populateEmojis } from '@/misc/populate-emojis.js';
|
import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { Cache } from '@/misc/cache.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { Instance } from '../entities/instance.js';
|
import { isActor, getApId } from '@/remote/activitypub/type.js';
|
||||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
import DbResolver from '@/remote/activitypub/db-resolver.js';
|
||||||
|
import Resolver from '@/remote/activitypub/resolver.js';
|
||||||
|
import { createPerson } from '@/remote/activitypub/models/person.js';
|
||||||
|
import {
|
||||||
|
AnnouncementReads,
|
||||||
|
Announcements,
|
||||||
|
AntennaNotes,
|
||||||
|
Blockings,
|
||||||
|
ChannelFollowings,
|
||||||
|
DriveFiles,
|
||||||
|
Followings,
|
||||||
|
FollowRequests,
|
||||||
|
Instances,
|
||||||
|
MessagingMessages,
|
||||||
|
Mutings,
|
||||||
|
Notes,
|
||||||
|
NoteUnreads,
|
||||||
|
Notifications,
|
||||||
|
Pages,
|
||||||
|
UserGroupJoinings,
|
||||||
|
UserNotePinings,
|
||||||
|
UserProfiles,
|
||||||
|
UserSecurityKeys,
|
||||||
|
} from '../index.js';
|
||||||
|
import type { Instance } from '../entities/instance.js';
|
||||||
|
|
||||||
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
||||||
|
|
||||||
|
@ -33,12 +60,24 @@ const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]
|
||||||
|
|
||||||
function isLocalUser(user: User): user is ILocalUser;
|
function isLocalUser(user: User): user is ILocalUser;
|
||||||
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
function isLocalUser<T extends { host: User['host'] }>(user: T): user is T & { host: null; };
|
||||||
|
/**
|
||||||
|
* Returns true if the user is local.
|
||||||
|
*
|
||||||
|
* @param user The user to check.
|
||||||
|
* @returns True if the user is local.
|
||||||
|
*/
|
||||||
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
function isLocalUser(user: User | { host: User['host'] }): boolean {
|
||||||
return user.host == null;
|
return user.host == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRemoteUser(user: User): user is IRemoteUser;
|
function isRemoteUser(user: User): user is IRemoteUser;
|
||||||
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & { host: string; };
|
||||||
|
/**
|
||||||
|
* Returns true if the user is remote.
|
||||||
|
*
|
||||||
|
* @param user The user to check.
|
||||||
|
* @returns True if the user is remote.
|
||||||
|
*/
|
||||||
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
function isRemoteUser(user: User | { host: User['host'] }): boolean {
|
||||||
return !isLocalUser(user);
|
return !isLocalUser(user);
|
||||||
}
|
}
|
||||||
|
@ -156,6 +195,27 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
return count > 0;
|
return count > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async userFromURI(uri: string): Promise<User | null> {
|
||||||
|
const dbResolver = new DbResolver();
|
||||||
|
let local = await dbResolver.getUserFromApId(uri);
|
||||||
|
if (local) {
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetching Object once from remote
|
||||||
|
const resolver = new Resolver();
|
||||||
|
const object = await resolver.resolve(uri) as any;
|
||||||
|
|
||||||
|
// /@user If a URI other than the id is specified,
|
||||||
|
// the URI is determined here
|
||||||
|
if (uri !== object.id) {
|
||||||
|
local = await dbResolver.getUserFromApId(object.id);
|
||||||
|
if (local != null) return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isActor(object) ? await createPerson(getApId(object)) : null;
|
||||||
|
},
|
||||||
|
|
||||||
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||||
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
|
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
|
||||||
|
|
||||||
|
@ -320,6 +380,8 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
uri: user.uri,
|
uri: user.uri,
|
||||||
|
movedToUri: user.movedToUri ? await this.userFromURI(user.movedToUri) : null,
|
||||||
|
alsoKnownAs: user.alsoKnownAs,
|
||||||
createdAt: user.createdAt.toISOString(),
|
createdAt: user.createdAt.toISOString(),
|
||||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||||
|
|
|
@ -96,6 +96,16 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
format: 'uri',
|
format: 'uri',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
},
|
},
|
||||||
|
movedToUri: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uri',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
|
alsoKnownAs: {
|
||||||
|
type: 'array',
|
||||||
|
format: 'uri',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||||
|
|
||||||
const logger = new Logger('inbox');
|
const logger = new Logger('inbox');
|
||||||
|
|
||||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
// Processing when an activity arrives in the user's inbox
|
||||||
export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
const signature = job.data.signature; // HTTP-signature
|
const signature = job.data.signature; // HTTP-signature
|
||||||
const activity = job.data.activity;
|
const activity = job.data.activity;
|
||||||
|
@ -30,16 +30,15 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
delete info['@context'];
|
delete info['@context'];
|
||||||
logger.debug(JSON.stringify(info, null, 2));
|
logger.debug(JSON.stringify(info, null, 2));
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const host = toPuny(new URL(signature.keyId).hostname);
|
const host = toPuny(new URL(signature.keyId).hostname);
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// interrupt if blocked
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
if (meta.blockedHosts.includes(host)) {
|
if (meta.blockedHosts.includes(host)) {
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非公開モードなら許可なインスタンスのみ
|
// only whitelisted instances in private mode
|
||||||
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
|
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
@ -51,7 +50,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
|
|
||||||
const dbResolver = new DbResolver();
|
const dbResolver = new DbResolver();
|
||||||
|
|
||||||
// HTTP-Signature keyIdを元にDBから取得
|
// HTTP-Signature keyId from DB
|
||||||
let authUser: {
|
let authUser: {
|
||||||
user: CacheableRemoteUser;
|
user: CacheableRemoteUser;
|
||||||
key: UserPublickey | null;
|
key: UserPublickey | null;
|
||||||
|
@ -62,7 +61,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
|
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 対象が4xxならスキップ
|
// Skip if target is 4xx
|
||||||
if (e instanceof StatusError) {
|
if (e instanceof StatusError) {
|
||||||
if (e.isClientError) {
|
if (e.isClientError) {
|
||||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import DbResolver from '../../db-resolver.js';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
|
||||||
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
|
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
|
||||||
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
|
// ※ There is a block target in activity.object, which should be a local user that exists.
|
||||||
|
|
||||||
const dbResolver = new DbResolver();
|
const dbResolver = new DbResolver();
|
||||||
const blockee = await dbResolver.getUserFromApId(activity.object);
|
const blockee = await dbResolver.getUserFromApId(activity.object);
|
||||||
|
@ -15,7 +15,7 @@ export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<str
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blockee.host != null) {
|
if (blockee.host != null) {
|
||||||
return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
|
return `skip: The user you are trying to block is not a local user`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));
|
await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));
|
||||||
|
|
|
@ -1,5 +1,26 @@
|
||||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js';
|
import type { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { toArray } from '@/prelude/array.js';
|
||||||
|
import {
|
||||||
|
isCreate,
|
||||||
|
isDelete,
|
||||||
|
isUpdate,
|
||||||
|
isRead,
|
||||||
|
isFollow,
|
||||||
|
isAccept,
|
||||||
|
isReject,
|
||||||
|
isAdd,
|
||||||
|
isRemove,
|
||||||
|
isAnnounce,
|
||||||
|
isLike,
|
||||||
|
isUndo,
|
||||||
|
isBlock,
|
||||||
|
isCollectionOrOrderedCollection,
|
||||||
|
isCollection,
|
||||||
|
isFlag,
|
||||||
|
isMove,
|
||||||
|
} from '../type.js';
|
||||||
|
import { apLogger } from '../logger.js';
|
||||||
|
import Resolver from '../resolver.js';
|
||||||
import create from './create/index.js';
|
import create from './create/index.js';
|
||||||
import performDeleteActivity from './delete/index.js';
|
import performDeleteActivity from './delete/index.js';
|
||||||
import performUpdateActivity from './update/index.js';
|
import performUpdateActivity from './update/index.js';
|
||||||
|
@ -14,10 +35,8 @@ import add from './add/index.js';
|
||||||
import remove from './remove/index.js';
|
import remove from './remove/index.js';
|
||||||
import block from './block/index.js';
|
import block from './block/index.js';
|
||||||
import flag from './flag/index.js';
|
import flag from './flag/index.js';
|
||||||
import { apLogger } from '../logger.js';
|
import move from './move/index.js';
|
||||||
import Resolver from '../resolver.js';
|
import type { IObject } from '../type.js';
|
||||||
import { toArray } from '@/prelude/array.js';
|
|
||||||
import { Users } from '@/models/index.js';
|
|
||||||
|
|
||||||
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
|
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
|
@ -68,6 +87,8 @@ async function performOneActivity(actor: CacheableRemoteUser, activity: IObject)
|
||||||
await block(actor, activity);
|
await block(actor, activity);
|
||||||
} else if (isFlag(activity)) {
|
} else if (isFlag(activity)) {
|
||||||
await flag(actor, activity);
|
await flag(actor, activity);
|
||||||
|
} else if (isMove(activity)) {
|
||||||
|
await move(actor,activity);
|
||||||
} else {
|
} else {
|
||||||
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||||
}
|
}
|
||||||
|
|
76
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
76
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import type { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
|
import { IRemoteUser, User } from '@/models/entities/user.js';
|
||||||
|
import DbResolver from '@/remote/activitypub/db-resolver.js';
|
||||||
|
import { getRemoteUser } from '@/server/api/common/getters.js';
|
||||||
|
import { updatePerson } from '@/remote/activitypub/models/person.js';
|
||||||
|
import { Followings, Users } from '@/models/index.js';
|
||||||
|
import { makePaginationQuery } from '@/server/api/common/make-pagination-query.js';
|
||||||
|
import deleteFollowing from '@/services/following/delete.js';
|
||||||
|
import create from '@/services/following/create.js';
|
||||||
|
import { getUser } from '@/server/api/common/getters.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import { meta } from '@/server/api/endpoints/following/create.js';
|
||||||
|
import { IObject, IActor } from '../../type.js';
|
||||||
|
import type { IMove } from '../../type.js';
|
||||||
|
import Resolver from '@/remote/activitypub/resolver.js';
|
||||||
|
|
||||||
|
export default async (actor: CacheableRemoteUser, activity: IMove): Promise<string> => {
|
||||||
|
// ※ There is a block target in activity.object, which should be a local user that exists.
|
||||||
|
|
||||||
|
const dbResolver = new DbResolver();
|
||||||
|
const resolver = new Resolver();
|
||||||
|
let new_acc = await dbResolver.getUserFromApId(activity.target);
|
||||||
|
let actor_new;
|
||||||
|
if (!new_acc) actor_new = await resolver.resolve(<string>activity.target) as IActor;
|
||||||
|
|
||||||
|
if ((!new_acc || new_acc.uri === null) && (!actor_new || actor_new.id === null)) {
|
||||||
|
return 'move: new acc not found';
|
||||||
|
}
|
||||||
|
|
||||||
|
let newUri: string | null | undefined
|
||||||
|
newUri = new_acc ? new_acc.uri :
|
||||||
|
actor_new?.url?.toString();
|
||||||
|
|
||||||
|
if(newUri === null || newUri === undefined) return 'move: new acc not found #2';
|
||||||
|
|
||||||
|
await updatePerson(newUri);
|
||||||
|
await updatePerson(actor.uri!);
|
||||||
|
|
||||||
|
new_acc = await dbResolver.getUserFromApId(newUri);
|
||||||
|
let old = await dbResolver.getUserFromApId(actor.uri!);
|
||||||
|
|
||||||
|
if (old === null || old.uri === null || !new_acc?.alsoKnownAs?.includes(old.uri)) return 'move: accounts invalid';
|
||||||
|
|
||||||
|
old.movedToUri = new_acc.uri;
|
||||||
|
|
||||||
|
const followee = await getUser(actor.id).catch(e => {
|
||||||
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const followeeNew = await getUser(new_acc.id).catch(e => {
|
||||||
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const followings = await Followings.findBy({
|
||||||
|
followeeId: followee.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
followings.forEach(async following => {
|
||||||
|
//if follower is local
|
||||||
|
if (!following.followerHost) {
|
||||||
|
const follower = await getUser(following.followerId).catch(e => {
|
||||||
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
await deleteFollowing(follower!, followee);
|
||||||
|
try {
|
||||||
|
await create(follower!, followeeNew);
|
||||||
|
} catch (e) { /* empty */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return 'ok';
|
||||||
|
};
|
|
@ -172,6 +172,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
name: truncate(person.name, nameLength),
|
name: truncate(person.name, nameLength),
|
||||||
isLocked: !!person.manuallyApprovesFollowers,
|
isLocked: !!person.manuallyApprovesFollowers,
|
||||||
|
movedToUri: person.movedTo,
|
||||||
|
alsoKnownAs: person.alsoKnownAs,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
username: person.preferredUsername,
|
username: person.preferredUsername,
|
||||||
usernameLower: person.preferredUsername!.toLowerCase(),
|
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||||
|
@ -277,21 +279,21 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Personの情報を更新します。
|
* Update Person data from remote.
|
||||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
* If the target Person is not registered in Calckey, it is ignored.
|
||||||
* @param uri URI of Person
|
* @param uri URI of Person
|
||||||
* @param resolver Resolver
|
* @param resolver Resolver
|
||||||
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
* @param hint Hint of Person object (If this value is a valid Person, it is used for updating without Remote resolve)
|
||||||
*/
|
*/
|
||||||
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
|
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// Skip if the URI points to this server
|
||||||
if (uri.startsWith(config.url + '/')) {
|
if (uri.startsWith(config.url + '/')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region Already registered on this server?
|
||||||
const exist = await Users.findOneBy({ uri }) as IRemoteUser;
|
const exist = await Users.findOneBy({ uri }) as IRemoteUser;
|
||||||
|
|
||||||
if (exist == null) {
|
if (exist == null) {
|
||||||
|
@ -307,7 +309,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
|
|
||||||
logger.info(`Updating the Person: ${person.id}`);
|
logger.info(`Updating the Person: ${person.id}`);
|
||||||
|
|
||||||
// アバターとヘッダー画像をフェッチ
|
// Fetch avatar and header image
|
||||||
const [avatar, banner] = await Promise.all([
|
const [avatar, banner] = await Promise.all([
|
||||||
person.icon,
|
person.icon,
|
||||||
person.image,
|
person.image,
|
||||||
|
@ -317,7 +319,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
: resolveImage(exist, img).catch(() => null),
|
: resolveImage(exist, img).catch(() => null),
|
||||||
));
|
));
|
||||||
|
|
||||||
// カスタム絵文字取得
|
// Custom pictogram acquisition
|
||||||
const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
|
const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
|
||||||
logger.info(`extractEmojis: ${e}`);
|
logger.info(`extractEmojis: ${e}`);
|
||||||
return [] as Emoji[];
|
return [] as Emoji[];
|
||||||
|
@ -343,6 +345,8 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
isBot: getApType(object) === 'Service',
|
isBot: getApType(object) === 'Service',
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
isLocked: !!person.manuallyApprovesFollowers,
|
isLocked: !!person.manuallyApprovesFollowers,
|
||||||
|
movedToUri: person.movedTo,
|
||||||
|
alsoKnownAs: person.alsoKnownAs,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
} as Partial<User>;
|
} as Partial<User>;
|
||||||
|
|
||||||
|
@ -374,10 +378,10 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
|
|
||||||
publishInternalEvent('remoteUserUpdated', { id: exist.id });
|
publishInternalEvent('remoteUserUpdated', { id: exist.id });
|
||||||
|
|
||||||
// ハッシュタグ更新
|
// Hashtag Update
|
||||||
updateUsertags(exist, tags);
|
updateUsertags(exist, tags);
|
||||||
|
|
||||||
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
// If the user in question is a follower, followers will also be updated.
|
||||||
await Followings.update({
|
await Followings.update({
|
||||||
followerId: exist.id,
|
followerId: exist.id,
|
||||||
}, {
|
}, {
|
||||||
|
@ -388,15 +392,15 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Personを解決します。
|
* Resolve Person.
|
||||||
*
|
*
|
||||||
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
|
* If the target person is registered in Calckey, it returns it;
|
||||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
* otherwise, it fetches it from the remote server, registers it in Calckey, and returns it.
|
||||||
*/
|
*/
|
||||||
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> {
|
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region If already registered on this server, return it.
|
||||||
const exist = await fetchPerson(uri);
|
const exist = await fetchPerson(uri);
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
|
@ -404,7 +408,7 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<C
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// Fetched from remote server and registered
|
||||||
if (resolver == null) resolver = new Resolver();
|
if (resolver == null) resolver = new Resolver();
|
||||||
return await createPerson(uri, resolver);
|
return await createPerson(uri, resolver);
|
||||||
}
|
}
|
||||||
|
@ -482,14 +486,14 @@ export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
|
||||||
// Resolve and regist Notes
|
// Resolve and regist Notes
|
||||||
const limit = promiseLimit<Note | null>(2);
|
const limit = promiseLimit<Note | null>(2);
|
||||||
const featuredNotes = await Promise.all(items
|
const featuredNotes = await Promise.all(items
|
||||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
.filter(item => getApType(item) === 'Note') // TODO: Maybe it doesn't have to be a Note.
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map(item => limit(() => resolveNote(item, resolver))));
|
.map(item => limit(() => resolveNote(item, resolver))));
|
||||||
|
|
||||||
await db.transaction(async transactionalEntityManager => {
|
await db.transaction(async transactionalEntityManager => {
|
||||||
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
||||||
|
|
||||||
// とりあえずidを別の時間で生成して順番を維持
|
// For now, generate the id at a different time and maintain the order.
|
||||||
let td = 0;
|
let td = 0;
|
||||||
for (const note of featuredNotes.filter(note => note != null)) {
|
for (const note of featuredNotes.filter(note => note != null)) {
|
||||||
td -= 1000;
|
td -= 1000;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import config from '@/config/index.js';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { IActivity } from '../type.js';
|
import config from '@/config/index.js';
|
||||||
import { LdSignature } from '../misc/ld-signature.js';
|
|
||||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import type { User } from '@/models/entities/user.js';
|
||||||
|
import { LdSignature } from '../misc/ld-signature.js';
|
||||||
|
import type { IActivity } from '../type.js';
|
||||||
|
|
||||||
export const renderActivity = (x: any): IActivity | null => {
|
export const renderActivity = (x: any): IActivity | null => {
|
||||||
if (x == null) return null;
|
if (x == null) return null;
|
||||||
|
@ -19,6 +19,7 @@ export const renderActivity = (x: any): IActivity | null => {
|
||||||
{
|
{
|
||||||
// as non-standards
|
// as non-standards
|
||||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||||
|
movedToUri: 'as:movedTo',
|
||||||
sensitive: 'as:sensitive',
|
sensitive: 'as:sensitive',
|
||||||
Hashtag: 'as:Hashtag',
|
Hashtag: 'as:Hashtag',
|
||||||
quoteUrl: 'as:quoteUrl',
|
quoteUrl: 'as:quoteUrl',
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import renderImage from './image.js';
|
|
||||||
import renderKey from './key.js';
|
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { ILocalUser } from '@/models/entities/user.js';
|
import type { ILocalUser } from '@/models/entities/user.js';
|
||||||
import { toHtml } from '../../../mfm/to-html.js';
|
|
||||||
import { getEmojis } from './note.js';
|
|
||||||
import renderEmoji from './emoji.js';
|
|
||||||
import { IIdentifier } from '../models/identifier.js';
|
|
||||||
import renderHashtag from './hashtag.js';
|
|
||||||
import { DriveFiles, UserProfiles } from '@/models/index.js';
|
import { DriveFiles, UserProfiles } from '@/models/index.js';
|
||||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||||
|
import { toHtml } from '../../../mfm/to-html.js';
|
||||||
|
import renderImage from './image.js';
|
||||||
|
import renderKey from './key.js';
|
||||||
|
import { getEmojis } from './note.js';
|
||||||
|
import renderEmoji from './emoji.js';
|
||||||
|
import renderHashtag from './hashtag.js';
|
||||||
|
import type { IIdentifier } from '../models/identifier.js';
|
||||||
|
|
||||||
export async function renderPerson(user: ILocalUser) {
|
export async function renderPerson(user: ILocalUser) {
|
||||||
const id = `${config.url}/users/${user.id}`;
|
const id = `${config.url}/users/${user.id}`;
|
||||||
|
@ -71,17 +71,19 @@ export async function renderPerson(user: ILocalUser) {
|
||||||
image: banner ? renderImage(banner) : null,
|
image: banner ? renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
manuallyApprovesFollowers: user.isLocked,
|
manuallyApprovesFollowers: user.isLocked,
|
||||||
|
movedTo: user.movedToUri,
|
||||||
|
alsoKnownAs: user.alsoKnownAs,
|
||||||
discoverable: !!user.isExplorable,
|
discoverable: !!user.isExplorable,
|
||||||
publicKey: renderKey(user, keypair, `#main-key`),
|
publicKey: renderKey(user, keypair, '#main-key'),
|
||||||
isCat: user.isCat,
|
isCat: user.isCat,
|
||||||
attachment: attachment.length ? attachment : undefined,
|
attachment: attachment.length ? attachment : undefined,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
if (profile?.birthday) {
|
if (profile.birthday) {
|
||||||
person['vcard:bday'] = profile.birthday;
|
person['vcard:bday'] = profile.birthday;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile?.location) {
|
if (profile.location) {
|
||||||
person['vcard:Address'] = profile.location;
|
person['vcard:Address'] = profile.location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,9 +156,11 @@ export interface IActor extends IObject {
|
||||||
name?: string;
|
name?: string;
|
||||||
preferredUsername?: string;
|
preferredUsername?: string;
|
||||||
manuallyApprovesFollowers?: boolean;
|
manuallyApprovesFollowers?: boolean;
|
||||||
|
movedTo?: string;
|
||||||
|
alsoKnownAs?: string[];
|
||||||
discoverable?: boolean;
|
discoverable?: boolean;
|
||||||
inbox: string;
|
inbox: string;
|
||||||
sharedInbox?: string; // 後方互換性のため
|
sharedInbox?: string; // backward compatibility.. ig
|
||||||
publicKey?: {
|
publicKey?: {
|
||||||
id: string;
|
id: string;
|
||||||
publicKeyPem: string;
|
publicKeyPem: string;
|
||||||
|
@ -279,6 +281,11 @@ export interface IFlag extends IActivity {
|
||||||
type: 'Flag';
|
type: 'Flag';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMove extends IActivity {
|
||||||
|
type: 'Move';
|
||||||
|
target: IObject | string;
|
||||||
|
}
|
||||||
|
|
||||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||||
|
@ -293,3 +300,4 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
|
||||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||||
|
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import webFinger from './webfinger.js';
|
|
||||||
import config from '@/config/index.js';
|
|
||||||
import { createPerson, updatePerson } from './activitypub/models/person.js';
|
|
||||||
import { remoteLogger } from './logger.js';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
import { IsNull } from 'typeorm';
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
import type { User, IRemoteUser } from '@/models/entities/user.js';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
import { toPuny } from '@/misc/convert-host.js';
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
import { IsNull } from 'typeorm';
|
import webFinger from './webfinger.js';
|
||||||
|
import { createPerson, updatePerson } from './activitypub/models/person.js';
|
||||||
|
import { remoteLogger } from './logger.js';
|
||||||
|
|
||||||
const logger = remoteLogger.createSubLogger('resolve-user');
|
const logger = remoteLogger.createSubLogger('resolve-user');
|
||||||
|
|
||||||
|
@ -49,9 +49,9 @@ export async function resolveUser(username: string, host: string | null): Promis
|
||||||
return await createPerson(self.href);
|
return await createPerson(self.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
// If user information is out of date, return it by starting over from WebFilger
|
||||||
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
// Prevent multiple attempts to connect to unconnected instances, update before each attempt to prevent subsequent similar attempts
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
@ -67,7 +67,7 @@ export async function resolveUser(username: string, host: string | null): Promis
|
||||||
// validate uri
|
// validate uri
|
||||||
const uri = new URL(self.href);
|
const uri = new URL(self.href);
|
||||||
if (uri.hostname !== host) {
|
if (uri.hostname !== host) {
|
||||||
throw new Error(`Invalid uri`);
|
throw new Error('Invalid uri');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update({
|
await Users.update({
|
||||||
|
|
|
@ -38,6 +38,7 @@ function inbox(ctx: Router.RouterContext) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
processInbox(ctx.request.body, signature);
|
processInbox(ctx.request.body, signature);
|
||||||
|
|
||||||
ctx.status = 202;
|
ctx.status = 202;
|
||||||
|
@ -86,7 +87,7 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートだったらリダイレクト
|
// redirect if remote
|
||||||
if (note.userHost != null) {
|
if (note.userHost != null) {
|
||||||
if (note.uri == null || isSelfHost(note.userHost)) {
|
if (note.uri == null || isSelfHost(note.userHost)) {
|
||||||
ctx.status = 500;
|
ctx.status = 500;
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
followerId: user.id,
|
followerId: user.id,
|
||||||
} as FindOptionsWhere<Following>;
|
} as FindOptionsWhere<Following>;
|
||||||
|
|
||||||
// カーソルが指定されている場合
|
// If a cursor is specified
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
query.id = LessThan(cursor);
|
query.id = LessThan(cursor);
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
order: { id: -1 },
|
order: { id: -1 },
|
||||||
});
|
});
|
||||||
|
|
||||||
// 「次のページ」があるかどうか
|
// Whether there is a "next page" or not
|
||||||
const inStock = followings.length === limit + 1;
|
const inStock = followings.length === limit + 1;
|
||||||
if (inStock) followings.pop();
|
if (inStock) followings.pop();
|
||||||
|
|
||||||
|
|
|
@ -306,6 +306,7 @@ import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.j
|
||||||
import * as ep___users_groups_update from './endpoints/users/groups/update.js';
|
import * as ep___users_groups_update from './endpoints/users/groups/update.js';
|
||||||
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
|
||||||
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
|
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
|
||||||
|
import * as ep___users_lists_delete_all from './endpoints/users/lists/delete-all.js';
|
||||||
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
|
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
|
||||||
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
|
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
|
||||||
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
||||||
|
@ -324,6 +325,10 @@ import * as ep___users_stats from './endpoints/users/stats.js';
|
||||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||||
import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
|
import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
|
||||||
|
|
||||||
|
//Calckey Move
|
||||||
|
import * as ep___i_move from './endpoints/i/move.js';
|
||||||
|
import * as ep___i_known_as from './endpoints/i/known-as.js';
|
||||||
|
|
||||||
const eps = [
|
const eps = [
|
||||||
['admin/meta', ep___admin_meta],
|
['admin/meta', ep___admin_meta],
|
||||||
['admin/abuse-user-reports', ep___admin_abuseUserReports],
|
['admin/abuse-user-reports', ep___admin_abuseUserReports],
|
||||||
|
@ -488,6 +493,8 @@ const eps = [
|
||||||
['hashtags/trend', ep___hashtags_trend],
|
['hashtags/trend', ep___hashtags_trend],
|
||||||
['hashtags/users', ep___hashtags_users],
|
['hashtags/users', ep___hashtags_users],
|
||||||
['i', ep___i],
|
['i', ep___i],
|
||||||
|
['i/known-as', ep___i_known_as],
|
||||||
|
['i/move', ep___i_move],
|
||||||
['i/2fa/done', ep___i_2fa_done],
|
['i/2fa/done', ep___i_2fa_done],
|
||||||
['i/2fa/key-done', ep___i_2fa_keyDone],
|
['i/2fa/key-done', ep___i_2fa_keyDone],
|
||||||
['i/2fa/password-less', ep___i_2fa_passwordLess],
|
['i/2fa/password-less', ep___i_2fa_passwordLess],
|
||||||
|
@ -631,6 +638,7 @@ const eps = [
|
||||||
['users/groups/update', ep___users_groups_update],
|
['users/groups/update', ep___users_groups_update],
|
||||||
['users/lists/create', ep___users_lists_create],
|
['users/lists/create', ep___users_lists_create],
|
||||||
['users/lists/delete', ep___users_lists_delete],
|
['users/lists/delete', ep___users_lists_delete],
|
||||||
|
['users/lists/delete-all', ep___users_lists_delete_all],
|
||||||
['users/lists/list', ep___users_lists_list],
|
['users/lists/list', ep___users_lists_list],
|
||||||
['users/lists/pull', ep___users_lists_pull],
|
['users/lists/pull', ep___users_lists_pull],
|
||||||
['users/lists/push', ep___users_lists_push],
|
['users/lists/push', ep___users_lists_push],
|
||||||
|
|
|
@ -88,10 +88,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* URIからUserかNoteを解決する
|
* Resolve User or Note from URI
|
||||||
*/
|
*/
|
||||||
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||||
// ブロックしてたら中断
|
// Wait if blocked.
|
||||||
const fetchedMeta = await fetchMeta();
|
const fetchedMeta = await fetchMeta();
|
||||||
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
|
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
|
||||||
|
|
||||||
|
@ -103,12 +103,12 @@ async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined):
|
||||||
]));
|
]));
|
||||||
if (local != null) return local;
|
if (local != null) return local;
|
||||||
|
|
||||||
// リモートから一旦オブジェクトフェッチ
|
// fetching Object once from remote
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
const object = await resolver.resolve(uri) as any;
|
const object = await resolver.resolve(uri) as any;
|
||||||
|
|
||||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
// /@user If a URI other than the id is specified,
|
||||||
// これはDBに存在する可能性があるため再度DB検索
|
// the URI is determined here
|
||||||
if (uri !== object.id) {
|
if (uri !== object.id) {
|
||||||
local = await mergePack(me, ...await Promise.all([
|
local = await mergePack(me, ...await Promise.all([
|
||||||
dbResolver.getUserFromApId(object.id),
|
dbResolver.getUserFromApId(object.id),
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: false,
|
||||||
requireCredentialPrivateMode: true,
|
requireCredentialPrivateMode: true,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import define from '../../../define.js';
|
|
||||||
import { GalleryPosts } from '@/models/index.js';
|
import { GalleryPosts } from '@/models/index.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -33,7 +33,7 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
||||||
.andWhere(`post.userId = :meId`, { meId: user.id });
|
.andWhere('post.userId = :meId', { meId: user.id });
|
||||||
|
|
||||||
const posts = await query
|
const posts = await query
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
|
|
95
packages/backend/src/server/api/endpoints/i/known-as.ts
Normal file
95
packages/backend/src/server/api/endpoints/i/known-as.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import type { User, UserDetailedNotMeOnly } from '@/models/entities/user.js';
|
||||||
|
import { Users } from '@/models/index.js';
|
||||||
|
import { resolveUser } from '@/remote/resolve-user.js';
|
||||||
|
import acceptAllFollowRequests from '@/services/following/requests/accept-all.js';
|
||||||
|
import { publishToFollowers } from '@/services/i/update.js';
|
||||||
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
|
import { DAY } from '@/const.js';
|
||||||
|
import { apiLogger } from '../../logger.js';
|
||||||
|
import { UserProfiles } from '@/models/index.js';
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['users'],
|
||||||
|
|
||||||
|
secure: true,
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: DAY,
|
||||||
|
max: 30,
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
||||||
|
},
|
||||||
|
notRemote: {
|
||||||
|
message: 'User is not remote. You can only migrate to other instances.',
|
||||||
|
code: 'NOT_REMOTE',
|
||||||
|
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
alsoKnownAs: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['alsoKnownAs'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
if (!ps.alsoKnownAs) throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
|
||||||
|
let unfiltered: string = ps.alsoKnownAs;
|
||||||
|
|
||||||
|
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
|
||||||
|
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
|
||||||
|
|
||||||
|
const userAddress: string[] = unfiltered.split('@');
|
||||||
|
|
||||||
|
const knownAs: UserDetailedNotMeOnly = await resolveUser(userAddress[0], userAddress[1]).catch(e => {
|
||||||
|
apiLogger.warn(`failed to resolve remote user: ${e}`);
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
const profileTo = await UserProfiles.findOneByOrFail({ userId: knownAs.id });
|
||||||
|
let toUrl: string | null = profileTo.url;
|
||||||
|
if(!toUrl) {
|
||||||
|
toUrl = `${config.url}/@${knownAs.username}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = {} as Partial<User>;
|
||||||
|
|
||||||
|
if (!toUrl) toUrl = '';
|
||||||
|
if (updates.alsoKnownAs == null || updates.alsoKnownAs.length === 0) {
|
||||||
|
updates.alsoKnownAs = [toUrl];
|
||||||
|
} else {
|
||||||
|
updates.alsoKnownAs.push(toUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Users.update(user.id, updates);
|
||||||
|
|
||||||
|
const iObj = await Users.pack<true, true>(user.id, user, {
|
||||||
|
detail: true,
|
||||||
|
includeSecrets: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Publish meUpdated event
|
||||||
|
publishMainStream(user.id, 'meUpdated', iObj);
|
||||||
|
|
||||||
|
if (user.isLocked === false) {
|
||||||
|
acceptAllFollowRequests(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
publishToFollowers(user.id);
|
||||||
|
|
||||||
|
return iObj;
|
||||||
|
});
|
151
packages/backend/src/server/api/endpoints/i/move.ts
Normal file
151
packages/backend/src/server/api/endpoints/i/move.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import type { User } from '@/models/entities/user.js';
|
||||||
|
import { resolveUser } from '@/remote/resolve-user.js';
|
||||||
|
import { DAY } from '@/const.js';
|
||||||
|
import DeliverManager from '@/remote/activitypub/deliver-manager.js';
|
||||||
|
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||||
|
import { genId } from '@/misc/gen-id.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
import { apiLogger } from '../../logger.js';
|
||||||
|
import deleteFollowing from '@/services/following/delete.js';
|
||||||
|
import create from '@/services/following/create.js';
|
||||||
|
import { getUser } from '@/server/api/common/getters.js';
|
||||||
|
import { Followings, Users } from '@/models/index.js';
|
||||||
|
import { UserProfiles } from '@/models/index.js';
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['users'],
|
||||||
|
|
||||||
|
secure: true,
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: DAY,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchMoveTarget: {
|
||||||
|
message: 'No such move target.',
|
||||||
|
code: 'NO_SUCH_MOVE_TARGET',
|
||||||
|
id: 'b5c90186-4ab0-49c8-9bba-a1f76c202ba4',
|
||||||
|
},
|
||||||
|
remoteAccountForbids: {
|
||||||
|
message: 'Remote account doesn\'t have proper \'Known As\' alias. Did you remember to set it?',
|
||||||
|
code: 'REMOTE_ACCOUNT_FORBIDS',
|
||||||
|
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
|
||||||
|
},
|
||||||
|
notRemote: {
|
||||||
|
message: 'User is not remote. You can only migrate to other instances.',
|
||||||
|
code: 'NOT_REMOTE',
|
||||||
|
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
||||||
|
},
|
||||||
|
adminForbidden: {
|
||||||
|
message: 'Admins cant migrate.',
|
||||||
|
code: 'NOT_ADMIN_FORBIDDEN',
|
||||||
|
id: '4362e8dc-731f-4ad8-a694-be2a88922a24',
|
||||||
|
},
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
moveToAccount: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['moveToAccount'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
function moveActivity(toUrl: string, fromUrl: string) {
|
||||||
|
const activity = {
|
||||||
|
id: genId(),
|
||||||
|
actor: fromUrl,
|
||||||
|
type: 'Move',
|
||||||
|
object: fromUrl,
|
||||||
|
target: toUrl,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
return renderActivity(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||||
|
if (user.isAdmin) throw new ApiError(meta.errors.adminForbidden);
|
||||||
|
|
||||||
|
let unfiltered: string = ps.moveToAccount;
|
||||||
|
|
||||||
|
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
|
||||||
|
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.notRemote);
|
||||||
|
const userAddress: string[] = unfiltered.split('@');
|
||||||
|
|
||||||
|
const moveTo: User = await resolveUser(userAddress[0], userAddress[1]).catch(e => {
|
||||||
|
apiLogger.warn(`failed to resolve remote user: ${e}`);
|
||||||
|
throw new ApiError(meta.errors.noSuchMoveTarget);
|
||||||
|
});
|
||||||
|
const profileFrom = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
let fromUrl: string | null = profileFrom.url;
|
||||||
|
if(!fromUrl) {
|
||||||
|
fromUrl = `${config.url}/@${user.username}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileTo = await UserProfiles.findOneByOrFail({ userId: moveTo.id });
|
||||||
|
let toUrl: string | null = profileTo.url;
|
||||||
|
if(!toUrl) {
|
||||||
|
toUrl = `${config.url}/@${moveTo.username}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allowed = false;
|
||||||
|
|
||||||
|
moveTo.alsoKnownAs?.forEach(element => {
|
||||||
|
if (fromUrl!.includes(element)) allowed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allowed || !toUrl || !fromUrl) throw new ApiError(meta.errors.remoteAccountForbids);
|
||||||
|
|
||||||
|
const updates = {} as Partial<User>;
|
||||||
|
|
||||||
|
if (!toUrl) toUrl = '';
|
||||||
|
updates.movedToUri = toUrl;
|
||||||
|
|
||||||
|
await Users.update(user.id, updates);
|
||||||
|
const iObj = await Users.pack<true, true>(user.id, user, {
|
||||||
|
detail: true,
|
||||||
|
includeSecrets: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moveAct = moveActivity(toUrl, fromUrl);
|
||||||
|
const dm = new DeliverManager(user, moveAct);
|
||||||
|
dm.addFollowersRecipe();
|
||||||
|
dm.execute();
|
||||||
|
|
||||||
|
// Publish meUpdated event
|
||||||
|
publishMainStream(user.id, 'meUpdated', iObj);
|
||||||
|
|
||||||
|
const followings = await Followings.findBy({
|
||||||
|
followeeId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
followings.forEach(async following => {
|
||||||
|
//if follower is local
|
||||||
|
if (!following.followerHost) {
|
||||||
|
const follower = await getUser(following.followerId).catch(e => {
|
||||||
|
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
await deleteFollowing(follower!, user);
|
||||||
|
try {
|
||||||
|
await create(follower!, moveTo);
|
||||||
|
} catch (e) { /* empty */ }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return iObj;
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { PageLikes } from '@/models/index.js';
|
import { PageLikes } from '@/models/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -26,7 +26,7 @@ export const meta = {
|
||||||
ref: 'Page',
|
ref: 'Page',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(PageLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
|
||||||
.andWhere(`like.userId = :meId`, { meId: user.id })
|
.andWhere('like.userId = :meId', { meId: user.id })
|
||||||
.leftJoinAndSelect('like.page', 'page');
|
.leftJoinAndSelect('like.page', 'page');
|
||||||
|
|
||||||
const likes = await query
|
const likes = await query
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { Pages } from '@/models/index.js';
|
import { Pages } from '@/models/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -33,7 +33,7 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
||||||
.andWhere(`page.userId = :meId`, { meId: user.id });
|
.andWhere('page.userId = :meId', { meId: user.id });
|
||||||
|
|
||||||
const pages = await query
|
const pages = await query
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
|
|
|
@ -78,6 +78,12 @@ export const meta = {
|
||||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||||
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
|
id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
accountLocked: {
|
||||||
|
message: 'You migrated. Your account is now locked.',
|
||||||
|
code: 'ACCOUNT_LOCKED',
|
||||||
|
id: 'd390d7e1-8a5e-46ed-b625-06271cafd3d3',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -163,6 +169,7 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
if(user.movedToUri) throw new ApiError(meta.errors.accountLocked);
|
||||||
let visibleUsers: User[] = [];
|
let visibleUsers: User[] = [];
|
||||||
if (ps.visibleUserIds) {
|
if (ps.visibleUserIds) {
|
||||||
visibleUsers = await Users.findBy({
|
visibleUsers = await Users.findBy({
|
||||||
|
@ -250,7 +257,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿を作成
|
// Create a post
|
||||||
const note = await create(user, {
|
const note = await create(user, {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
files: files,
|
files: files,
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { UserLists } from '@/models/index.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['lists'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:account',
|
||||||
|
|
||||||
|
description: 'Delete all lists of users.',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchList: {
|
||||||
|
message: 'No such list.',
|
||||||
|
code: 'NO_SUCH_LIST',
|
||||||
|
id: '78436795-db79-42f5-b1e2-55ea2cf19166',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
while (await UserLists.findOneBy({ userId: user.id }) != null) {
|
||||||
|
const userList = await UserLists.findOneBy({ userId: user.id });
|
||||||
|
if (userList == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchList);
|
||||||
|
}
|
||||||
|
await UserLists.delete(userList.id);
|
||||||
|
}
|
||||||
|
});
|
|
@ -19,7 +19,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { publishMainStream } from '@/services/stream.js';
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { envOption } from '../env.js';
|
import { envOption } from '@/env.js';
|
||||||
import activityPub from './activitypub.js';
|
import activityPub from './activitypub.js';
|
||||||
import nodeinfo from './nodeinfo.js';
|
import nodeinfo from './nodeinfo.js';
|
||||||
import wellKnown from './well-known.js';
|
import wellKnown from './well-known.js';
|
||||||
|
@ -164,5 +164,6 @@ export default () => new Promise(resolve => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
server.listen(config.port, resolve);
|
server.listen(config.port, resolve);
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,11 +11,11 @@ const router = new Router();
|
||||||
const nodeinfo2_1path = '/nodeinfo/2.1';
|
const nodeinfo2_1path = '/nodeinfo/2.1';
|
||||||
const nodeinfo2_0path = '/nodeinfo/2.0';
|
const nodeinfo2_0path = '/nodeinfo/2.0';
|
||||||
|
|
||||||
export const links = [/* (awaiting release) {
|
export const links = [{
|
||||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
|
rel: 'https://nodeinfo.diaspora.software/ns/schema/2.1',
|
||||||
href: config.url + nodeinfo2_1path
|
href: config.url + nodeinfo2_1path
|
||||||
}, */{
|
}, {
|
||||||
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
rel: 'https://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||||
href: config.url + nodeinfo2_0path,
|
href: config.url + nodeinfo2_0path,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ router.get(nodeinfo2_1path, async ctx => {
|
||||||
router.get(nodeinfo2_0path, async ctx => {
|
router.get(nodeinfo2_0path, async ctx => {
|
||||||
const base = await cache.fetch(null, () => nodeinfo2());
|
const base = await cache.fetch(null, () => nodeinfo2());
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
delete base.software.repository;
|
delete base.software.repository;
|
||||||
|
|
||||||
ctx.body = { version: '2.0', ...base };
|
ctx.body = { version: '2.0', ...base };
|
||||||
|
|
|
@ -232,8 +232,43 @@ const getFeed = async (acct: string) => {
|
||||||
return user && await packFeed(user);
|
return user && await packFeed(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
|
||||||
|
const reUser = new RegExp(`^/@(?<user>[^/]+?)(?:\.(?<feed>json|rss|atom))?(?:/(?<sub>[^/]+))?$`);
|
||||||
|
router.get(reUser, async (ctx, next) => {
|
||||||
|
const groups = reUser.exec(ctx.originalUrl)?.groups;
|
||||||
|
if (!groups) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.params = groups;
|
||||||
|
|
||||||
|
console.log(ctx, ctx.params)
|
||||||
|
if (groups.feed) {
|
||||||
|
if (groups.sub) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (groups.feed) {
|
||||||
|
case 'json':
|
||||||
|
await jsonFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
case 'rss':
|
||||||
|
await rssFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
case 'atom':
|
||||||
|
await atomFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await userPage(ctx, next);
|
||||||
|
});
|
||||||
|
|
||||||
// Atom
|
// Atom
|
||||||
router.get('/@:user.atom', async ctx => {
|
const atomFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// RSS
|
// RSS
|
||||||
router.get('/@:user.rss', async ctx => {
|
const rssFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
router.get('/@:user.json', async ctx => {
|
const jsonFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -266,19 +301,26 @@ router.get('/@:user.json', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
//#region SSR (for crawlers)
|
//#region SSR (for crawlers)
|
||||||
// User
|
// User
|
||||||
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
const userPage: Router.Middleware = async (ctx, next) => {
|
||||||
const { username, host } = Acct.parse(ctx.params.user);
|
const userParam = ctx.params.user;
|
||||||
|
const subParam = ctx.params.sub;
|
||||||
|
const { username, host } = Acct.parse(userParam);
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user != null) {
|
if (user === null) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
const me = profile.fields
|
const me = profile.fields
|
||||||
|
@ -287,22 +329,19 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
||||||
.map(field => field.value)
|
.map(field => field.value)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
await ctx.render('user', {
|
const userDetail = {
|
||||||
user, profile, me,
|
user, profile, me,
|
||||||
avatarUrl: await Users.getAvatarUrl(user),
|
avatarUrl: await Users.getAvatarUrl(user),
|
||||||
sub: ctx.params.sub,
|
sub: subParam,
|
||||||
instanceName: meta.name || 'Calckey',
|
instanceName: meta.name || 'Calckey',
|
||||||
icon: meta.iconUrl,
|
icon: meta.iconUrl,
|
||||||
themeColor: meta.themeColor,
|
themeColor: meta.themeColor,
|
||||||
privateMode: meta.privateMode,
|
privateMode: meta.privateMode,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
await ctx.render('user', userDetail);
|
||||||
ctx.set('Cache-Control', 'public, max-age=15');
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
} else {
|
};
|
||||||
// リモートユーザーなので
|
|
||||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/users/:user', async ctx => {
|
router.get('/users/:user', async ctx => {
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
|
|
|
@ -127,8 +127,8 @@ type Option = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; createdAt: User['createdAt']; }, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
|
export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; createdAt: User['createdAt']; }, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
|
||||||
// チャンネル外にリプライしたら対象のスコープに合わせる
|
// If you reply outside the channel, match the scope of the target.
|
||||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
||||||
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
|
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
|
||||||
if (data.reply.channelId) {
|
if (data.reply.channelId) {
|
||||||
data.channel = await Channels.findOneBy({ id: data.reply.channelId });
|
data.channel = await Channels.findOneBy({ id: data.reply.channelId });
|
||||||
|
@ -137,8 +137,8 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// チャンネル内にリプライしたら対象のスコープに合わせる
|
// When you reply in a channel, match the scope of the target
|
||||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
||||||
if (data.reply && (data.channel == null) && data.reply.channelId) {
|
if (data.reply && (data.channel == null) && data.reply.channelId) {
|
||||||
data.channel = await Channels.findOneBy({ id: data.reply.channelId });
|
data.channel = await Channels.findOneBy({ id: data.reply.channelId });
|
||||||
}
|
}
|
||||||
|
@ -150,37 +150,37 @@ export default async (user: { id: User['id']; username: User['username']; host:
|
||||||
if (data.channel != null) data.visibleUsers = [];
|
if (data.channel != null) data.visibleUsers = [];
|
||||||
if (data.channel != null) data.localOnly = true;
|
if (data.channel != null) data.localOnly = true;
|
||||||
|
|
||||||
// サイレンス
|
// enforce silent clients on server
|
||||||
if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
|
if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
|
// Reject if the target of the renote is a public range other than "Home or Entire".
|
||||||
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
|
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
|
||||||
return rej('Renote target is not public or home');
|
return rej('Renote target is not public or home');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renote対象がpublicではないならhomeにする
|
// If the target of the renote is not public, make it home.
|
||||||
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
|
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renote対象がfollowersならfollowersにする
|
// If the target of Renote is followers, make it followers.
|
||||||
if (data.renote && data.renote.visibility === 'followers') {
|
if (data.renote && data.renote.visibility === 'followers') {
|
||||||
data.visibility = 'followers';
|
data.visibility = 'followers';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返信対象がpublicではないならhomeにする
|
// If the reply target is not public, make it home.
|
||||||
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
|
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ローカルのみをRenoteしたらローカルのみにする
|
// Renote local only if you Renote local only.
|
||||||
if (data.renote && data.renote.localOnly && data.channel == null) {
|
if (data.renote && data.renote.localOnly && data.channel == null) {
|
||||||
data.localOnly = true;
|
data.localOnly = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ローカルのみにリプライしたらローカルのみにする
|
// If you reply to local only, make it local only.
|
||||||
if (data.reply && data.reply.localOnly && data.channel == null) {
|
if (data.reply && data.reply.localOnly && data.channel == null) {
|
||||||
data.localOnly = true;
|
data.localOnly = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import * as childProcess from 'child_process';
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import { SIGKILL } from 'constants';
|
import { SIGKILL } from 'constants';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"blurhash": "1.1.5",
|
"blurhash": "1.1.5",
|
||||||
"broadcast-channel": "4.18.1",
|
"broadcast-channel": "4.18.1",
|
||||||
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
|
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
|
||||||
|
"calckey-js": "^0.0.17",
|
||||||
"chart.js": "4.0.1",
|
"chart.js": "4.0.1",
|
||||||
"chartjs-adapter-date-fns": "2.0.1",
|
"chartjs-adapter-date-fns": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.5.1",
|
"chartjs-plugin-gradient": "0.5.1",
|
||||||
|
@ -34,7 +35,6 @@
|
||||||
"katex": "0.16.3",
|
"katex": "0.16.3",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.0",
|
||||||
"misskey-js": "0.0.14",
|
|
||||||
"photoswipe": "5.3.3",
|
"photoswipe": "5.3.3",
|
||||||
"prismjs": "1.29.0",
|
"prismjs": "1.29.0",
|
||||||
"punycode": "2.1.1",
|
"punycode": "2.1.1",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineAsyncComponent, reactive } from 'vue';
|
import { defineAsyncComponent, reactive } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
||||||
import { i18n } from './i18n';
|
import { i18n } from './i18n';
|
||||||
import { del, get, set } from '@/scripts/idb-proxy';
|
import { del, get, set } from '@/scripts/idb-proxy';
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import XWindow from '@/components/MkWindow.vue';
|
import XWindow from '@/components/MkWindow.vue';
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from '@/components/form/textarea.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Acct from 'misskey-js/built/acct';
|
import * as Acct from 'calckey-js/built/acct';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { acct } from '@/filters/user';
|
import { acct } from '@/filters/user';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted } from 'vue';
|
import { nextTick, onMounted } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import Cropper from 'cropperjs';
|
import Cropper from 'cropperjs';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import { concat } from '@/scripts/array';
|
import { concat } from '@/scripts/array';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from '@/filters/bytes';
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
||||||
import XFolder from '@/components/MkDrive.folder.vue';
|
import XFolder from '@/components/MkDrive.folder.vue';
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'calckey-js';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import XDrive from '@/components/MkDrive.vue';
|
import XDrive from '@/components/MkDrive.vue';
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import XDrive from '@/components/MkDrive.vue';
|
import XDrive from '@/components/MkDrive.vue';
|
||||||
import XWindow from '@/components/MkWindow.vue';
|
import XWindow from '@/components/MkWindow.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||||
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'calckey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
|
const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import * as Acct from 'misskey-js/built/acct';
|
import * as Acct from 'calckey-js/built/acct';
|
||||||
import MkSwitch from '@/components/ui/switch.vue';
|
import MkSwitch from '@/components/ui/switch.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
<template v-else><i class="ph-caret-down-bold ph-lg"></i></template>
|
<template v-else><i class="ph-caret-down-bold ph-lg"></i></template>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<transition :name="$store.state.animation ? 'folder-toggle' : ''"
|
<transition
|
||||||
|
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||||
@enter="enter"
|
@enter="enter"
|
||||||
@after-enter="afterEnter"
|
@after-enter="afterEnter"
|
||||||
@leave="leave"
|
@leave="leave"
|
||||||
|
@ -27,17 +28,18 @@ import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
const localStoragePrefix = 'ui:folder:';
|
const localStoragePrefix = 'ui:folder:';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
expanded: {
|
expanded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true,
|
||||||
},
|
},
|
||||||
persistKey: {
|
persistKey: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -51,7 +53,7 @@ export default defineComponent({
|
||||||
if (this.persistKey) {
|
if (this.persistKey) {
|
||||||
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
|
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
function getParentBg(el: Element | null): string {
|
function getParentBg(el: Element | null): string {
|
||||||
|
@ -91,7 +93,7 @@ export default defineComponent({
|
||||||
afterLeave(el) {
|
afterLeave(el) {
|
||||||
el.style.height = null;
|
el.style.height = null;
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted } from 'vue';
|
import { onBeforeUnmount, onMounted } from 'vue';
|
||||||
import type * as Misskey from 'misskey-js';
|
import type * as Misskey from 'calckey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import bytes from '@/filters/bytes';
|
import bytes from '@/filters/bytes';
|
||||||
import number from '@/filters/number';
|
import number from '@/filters/number';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
warn?: boolean;
|
warn?: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import VuePlyr from 'vue-plyr';
|
import VuePlyr from 'vue-plyr';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'calckey-js';
|
||||||
import { ColdDeviceStorage } from '@/store';
|
import { ColdDeviceStorage } from '@/store';
|
||||||
import 'vue-plyr/dist/vue-plyr.css';
|
import 'vue-plyr/dist/vue-plyr.css';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||||
import PhotoSwipe from 'photoswipe';
|
import PhotoSwipe from 'photoswipe';
|
||||||
import 'photoswipe/style.css';
|
import 'photoswipe/style.css';
|
||||||
|
@ -46,7 +46,7 @@ onMounted(() => {
|
||||||
src: media.url,
|
src: media.url,
|
||||||
w: media.properties.width,
|
w: media.properties.width,
|
||||||
h: media.properties.height,
|
h: media.properties.height,
|
||||||
alt: media.name,
|
alt: media.comment,
|
||||||
};
|
};
|
||||||
if (media.properties.orientation != null && media.properties.orientation >= 5) {
|
if (media.properties.orientation != null && media.properties.orientation >= 5) {
|
||||||
[item.w, item.h] = [item.h, item.w];
|
[item.w, item.h] = [item.h, item.w];
|
||||||
|
@ -89,9 +89,38 @@ onMounted(() => {
|
||||||
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
||||||
}
|
}
|
||||||
itemData.msrc = file.thumbnailUrl;
|
itemData.msrc = file.thumbnailUrl;
|
||||||
|
itemData.alt = file.comment;
|
||||||
itemData.thumbCropped = true;
|
itemData.thumbCropped = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
lightbox.on('uiRegister', () => {
|
||||||
|
lightbox.pswp.ui.registerElement({
|
||||||
|
name: 'altText',
|
||||||
|
className: 'pwsp__alt-text-container',
|
||||||
|
appendTo: 'wrapper',
|
||||||
|
onInit: (el, pwsp) => {
|
||||||
|
let textBox = document.createElement('p');
|
||||||
|
textBox.className = 'pwsp__alt-text';
|
||||||
|
el.appendChild(textBox);
|
||||||
|
|
||||||
|
let preventProp = function(ev: Event): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allow scrolling/text selection
|
||||||
|
el.onwheel = preventProp;
|
||||||
|
el.onclick = preventProp;
|
||||||
|
el.onpointerdown = preventProp;
|
||||||
|
el.onpointercancel = preventProp;
|
||||||
|
el.onpointermove = preventProp;
|
||||||
|
|
||||||
|
pwsp.on('change', () => {
|
||||||
|
textBox.textContent = pwsp.currSlide.data.alt?.trim();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
lightbox.init();
|
lightbox.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -193,4 +222,35 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
|
||||||
//z-index: v-bind(pswpZIndex);
|
//z-index: v-bind(pswpZIndex);
|
||||||
z-index: 2000000;
|
z-index: 2000000;
|
||||||
}
|
}
|
||||||
|
.pwsp__alt-text-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwsp__alt-text {
|
||||||
|
color: white;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
max-height: 10vh;
|
||||||
|
overflow-x: clip;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwsp__alt-text:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import VuePlyr from 'vue-plyr';
|
import VuePlyr from 'vue-plyr';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'calckey-js';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import 'vue-plyr/dist/vue-plyr.css';
|
import 'vue-plyr/dist/vue-plyr.css';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
30
packages/client/src/components/MkMoved.vue
Normal file
30
packages/client/src/components/MkMoved.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="msjugskd _block">
|
||||||
|
<i class="ph-airplane-takeoff-bold ph-lg" style="margin-right: 8px;"/>
|
||||||
|
{{ i18n.ts.accountMoved }}
|
||||||
|
<MkMention class="link" :username="acct" :host="host"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import MkMention from './MkMention.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
acct: string;
|
||||||
|
host: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.msjugskd {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--infoWarnBg);
|
||||||
|
color: var(--error);
|
||||||
|
|
||||||
|
> .link {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -107,7 +107,7 @@
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
import { computed, inject, onMounted, onUnmounted, reactive, 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 'misskey-js';
|
import type * as misskey from 'calckey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
|
|
|
@ -117,7 +117,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'calckey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import MkVisibility from '@/components/MkVisibility.vue';
|
import MkVisibility from '@/components/MkVisibility.vue';
|
||||||
import { notePage } from '@/filters/note';
|
import { notePage } from '@/filters/note';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
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 XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from '@/components/MkCwButton.vue';
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
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 XCwButton from '@/components/MkCwButton.vue';
|
import XCwButton from '@/components/MkCwButton.vue';
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { notificationTypes } from 'misskey-js';
|
import { notificationTypes } from 'calckey-js';
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from './form/switch.vue';
|
||||||
import MkInfo from './MkInfo.vue';
|
import MkInfo from './MkInfo.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
||||||
import { notificationTypes } from 'misskey-js';
|
import { notificationTypes } from 'calckey-js';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
import XList from '@/components/MkDateSeparatedList.vue';
|
import XList from '@/components/MkDateSeparatedList.vue';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1">
|
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1" :behavior="`${ui === 'deck' ? 'window' : null}`">
|
||||||
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
||||||
<article>
|
<article>
|
||||||
<header>
|
<header>
|
||||||
|
@ -7,29 +7,20 @@
|
||||||
</header>
|
</header>
|
||||||
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
||||||
<footer>
|
<footer>
|
||||||
<img class="icon" :src="page.user.avatarUrl"/>
|
<img class="icon" :src="page.user.avatarUrl" aria-label="none"/>
|
||||||
<p>{{ userName(page.user) }}</p>
|
<p>{{ userName(page.user) }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { userName } from '@/filters/user';
|
import { userName } from '@/filters/user';
|
||||||
import * as os from '@/os';
|
import { ui } from '@/config';
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps<{
|
||||||
props: {
|
page: any;
|
||||||
page: {
|
}>();
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
userName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
|
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onUnmounted, ref, toRef } from 'vue';
|
import { computed, onUnmounted, ref, toRef } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import { sum } from '@/scripts/array';
|
import { sum } from '@/scripts/array';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
|
@ -65,11 +65,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue';
|
import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
import { toASCII } from 'punycode/';
|
import { toASCII } from 'punycode/';
|
||||||
import * as Acct from 'misskey-js/built/acct';
|
import * as Acct from 'calckey-js/built/acct';
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import XNotePreview from '@/components/MkNotePreview.vue';
|
import XNotePreview from '@/components/MkNotePreview.vue';
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { Note } from 'misskey-js/built/entities';
|
import type { Note } from 'calckey-js/built/entities';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,10 @@ defineProps<{
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.jmgmzlwq {
|
.jmgmzlwq {
|
||||||
font-size: 0.8em;
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--infoWarnBg);
|
background: var(--infoWarnBg);
|
||||||
color: var(--infoWarnFg);
|
color: var(--infoWarnFg);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
> .link {
|
> .link {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'calckey-js';
|
||||||
import Ripple from '@/components/MkRipple.vue';
|
import Ripple from '@/components/MkRipple.vue';
|
||||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<XModalWindow
|
<XModalWindow
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="370"
|
:width="400"
|
||||||
:height="400"
|
|
||||||
@close="onClose"
|
@close="onClose"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required data-cy-signup-invitation-code @update:modelValue="onChangeInvitationCode">
|
||||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||||
<template #prefix><i class="ph-key-bold ph-lg"></i></template>
|
<template #prefix><i class="ph-key-bold ph-lg"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<div v-if="!instance.disableRegistration || (instance.disableRegistration && invitationState === 'entered')">
|
||||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
|
||||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
|
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
<template #suffix>@{{ host }}</template>
|
<template #suffix>@{{ host }}</template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
<span v-if="usernameState === 'wait'" style="color:#999"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
|
<span v-if="usernameState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
|
||||||
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
|
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
|
||||||
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
|
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
|
||||||
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
|
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
|
||||||
|
@ -22,7 +23,7 @@
|
||||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
|
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
|
||||||
<template #prefix><i class="ph-envelope-simple-open-bold ph-lg"></i></template>
|
<template #prefix><i class="ph-envelope-simple-open-bold ph-lg"></i></template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
<span v-if="emailState === 'wait'" style="color:#999"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
|
<span v-if="emailState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
|
||||||
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
|
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
|
||||||
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span>
|
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span>
|
||||||
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span>
|
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span>
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
|
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -99,6 +101,7 @@ let retypedPassword: string = $ref('');
|
||||||
let invitationCode: string = $ref('');
|
let invitationCode: string = $ref('');
|
||||||
let email = $ref('');
|
let email = $ref('');
|
||||||
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
|
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
|
||||||
|
let invitationState: null | 'entered' = $ref(null);
|
||||||
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
|
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
|
||||||
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
|
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
|
||||||
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
|
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
|
||||||
|
@ -115,6 +118,14 @@ const shouldDisableSubmitting = $computed((): boolean => {
|
||||||
passwordRetypeState === 'not-match';
|
passwordRetypeState === 'not-match';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onChangeInvitationCode(): void {
|
||||||
|
if (invitationCode === '') {
|
||||||
|
invitationState = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invitationState = 'entered';
|
||||||
|
}
|
||||||
|
|
||||||
function onChangeUsername(): void {
|
function onChangeUsername(): void {
|
||||||
if (username === '') {
|
if (username === '') {
|
||||||
usernameState = null;
|
usernameState = null;
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<XModalWindow
|
<XModalWindow
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="366"
|
:width="400"
|
||||||
:height="500"
|
@close="dialog!.close()"
|
||||||
@close="dialog.close()"
|
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.signup }}</template>
|
<template #header>{{ i18n.ts.signup }}</template>
|
||||||
|
|
||||||
<div class="_monolithic_">
|
<div class="_monolithic_">
|
||||||
<div class="_section">
|
<div class="_section">
|
||||||
<XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
|
@ -37,10 +36,10 @@ const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
function onSignup(res) {
|
function onSignup(res) {
|
||||||
emit('done', res);
|
emit('done', res);
|
||||||
dialog.close();
|
dialog?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSignupEmailPending() {
|
function onSignupEmailPending() {
|
||||||
dialog.close();
|
dialog?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Note } from 'misskey-js/built/entities';
|
import type { Note } from 'calckey-js/built/entities';
|
||||||
import Ripple from '@/components/MkRipple.vue';
|
import Ripple from '@/components/MkRipple.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import XMediaList from '@/components/MkMediaList.vue';
|
import XMediaList from '@/components/MkMediaList.vue';
|
||||||
import XPoll from '@/components/MkPoll.vue';
|
import XPoll from '@/components/MkPoll.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import { permissions as kinds } from 'misskey-js';
|
import { permissions as kinds } from 'calckey-js';
|
||||||
import MkInput from './form/input.vue';
|
import MkInput from './form/input.vue';
|
||||||
import MkSwitch from './form/switch.vue';
|
import MkSwitch from './form/switch.vue';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'calckey-js';
|
||||||
import MkMiniChart from '@/components/MkMiniChart.vue';
|
import MkMiniChart from '@/components/MkMiniChart.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { acct } from '@/filters/user';
|
import { acct } from '@/filters/user';
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue