@@ -11,6 +11,7 @@
+
# ✨ About Calckey
- Calckey is based off of Misskey, a powerful microblogging server on ActivityPub with features such as emoji reactions, a customizable web UI, rich chatting, and much more!
@@ -19,6 +20,7 @@
- Notable differences:
- Improved UI/UX (especially on mobile)
- Improved notifications
+ - Fediverse account migration
- Improved instance security
- Improved accessibility
- Recommended Instances timeline
@@ -49,10 +51,10 @@ This guide will work for both **starting from scratch** and **migrating from Mis
## 📦 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)
- 🐘 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
@@ -97,7 +99,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
## 💅 Customize
- To add custom CSS for all users, edit `./custom/assets/instance.css`.
-- To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`.
+- To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be available on `https://yourinstance.tld/static-assets/filename.ext`.
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
- To update custom assets without rebuilding, just run `yarn run gulp`.
@@ -134,24 +136,13 @@ cp -r ../misskey/files . # if you don't use object storage
```sh
# git pull
yarn install
-NODE_ENV=production yarn run build && yarn run migrate
+NODE_ENV=production yarn run rebuild && yarn run migrate
pm2 start "NODE_ENV=production yarn start" --name Calckey
```
-### 🐋 Prebuilt Docker image
+### 🐋 Docker
-```sh
-docker pull thatonecalculator/calckey
-docker up -d
-```
-
-### 🐳 Docker Compose
-
-```sh
-docker-compose build
-docker-compose run --rm web yarn run init
-docker-compose up -d
-```
+[How to run Calckey with Docker](./docker-README.md).
## 😉 Tips & Tricks
diff --git a/SECURITY.md b/SECURITY.md
index 2c026a5f33..848fa7cb7c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,9 +1,16 @@
# Reporting Security Issues
-If you discover a security issue in Misskey, please report it by sending an
-email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
+## Minor Security Issues
+
+If you discover a minor security issue in Calckey, please report it by sending an
+email to [kainoa@t1c.dev](mailto:kainoa@t1c.dev).
+
+## High Security Issues
+
+If you discover a security issue, which is so high risk, that too much is affected by it, please dont send it over unencrypted communication. You can share your PGP keys with us using kainoa@t1c.dev and after we established a secure communication, send it over E-Mail, or message us using matrix' encrypted private messages at @t1c:matrix.fedibird.com or @cleo:tchncs.de
+
This will allow us to assess the risk, and make a fix available before we add a
-bug report to the GitHub repository.
+bug report to the Codeberg repository.
-Thanks for helping make Misskey safe for everyone.
+Thanks for helping make Calckey safe for everyone.
diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml
new file mode 100644
index 0000000000..25c2e9ea9d
--- /dev/null
+++ b/dev/docker-compose.yml
@@ -0,0 +1,50 @@
+version: "3"
+
+services:
+ web:
+ image: docker.io/thatonecalculator/calckey
+ build: ..
+ restart: always
+ depends_on:
+ - db
+ - redis
+# - es
+ ports:
+ - "3000:3000"
+ networks:
+ - network
+ volumes:
+ - ../files:/calckey/files
+ - ../.config:/calckey/.config:ro
+
+ redis:
+ restart: always
+ image: docker.io/redis:7.0-alpine
+ networks:
+ - network
+ volumes:
+ - ../redis:/data
+
+ db:
+ restart: always
+ image: docker.io/postgres:12.2-alpine
+ networks:
+ - network
+ env_file:
+ - ../.config/docker.env
+ volumes:
+ - ../db:/var/lib/postgresql/data
+
+# es:
+# restart: always
+# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
+# environment:
+# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
+# - "TAKE_FILE_OWNERSHIP=111"
+# networks:
+# - network
+# volumes:
+# - ./elasticsearch:/usr/share/elasticsearch/data
+
+networks:
+ network:
diff --git a/docker-README.md b/docker-README.md
new file mode 100644
index 0000000000..cca4c011d8
--- /dev/null
+++ b/docker-README.md
@@ -0,0 +1,46 @@
+# 🐳 Docker Compose for Development
+
+```sh
+cd dev/
+docker-compose build
+docker-compose run --rm web yarn run init
+docker-compose up -d
+```
+
+# Running a Calckey instance with Docker
+
+## Pre-built docker container
+[thatonecalculator/calckey](https://hub.docker.com/r/thatonecalculator/calckey)
+## docker-compose
+You can find a `docker-compose.yml` file in the same folder as this `README`, along with a folder called `.config` containing two **example** files needed to get the instance running:
+- .config/docker.env (**db config settings**)
+- .config/default.yml (**calckey instance settings**)
+
+## configuring calckey
+
+Rename the files:
+
+`cp .config/default_example.yml .config/default.yml`
+
+`cp .config/example.env .config/docker.env`
+
+then edit them according to your environment.
+You can configure `docker.env` with anything you like, but you will have to pay attention to the `default.yml` file:
+- `url` should be set to the URL you will be hosting the web interface for the instance at.
+- `host`, `db`, `user`, `pass` will have to be configured in the `PostgreSQL configuration` section - `host` is the name of the postgres container (eg: *calckey_db_1*), and the others should match your `docker.env`.
+- `host`will need to be configured in the *Redis configuration* section - it is the name of the redis container (eg: *calckey_redis_1*)
+
+Everything else can be left as-is.
+
+## Running docker-compose
+The [prebuilt container for calckey](https://hub.docker.com/r/thatonecalculator/calckey) is fairly large, and may take a few minutes to download and extract using docker.
+
+Copy `docker-compose.yml` and the `config/` to a directory, then run the **docker-compose** command:
+`docker-compose up -d`.
+
+NOTE: This will take some time to come fully online, even after download and extracting the container images, and it may emit some error messages before completing successfully. Specifically, the `db` container needs to initialize and so isn't available to the `web` container right away. Only once the `db` container comes online does the `web` container start building and initializing the calckey tables.
+
+Once the instance is up you can use a web browser to access the web interface at `http://serverip:3000` (where `serverip` is the IP of the server you are running the calckey instance on).
+
+## Securing your instance with a reverse proxy
+On its own *calckey* serves itself with HTTP, and does not support SSL. In order to support encrypted connections via HTTPS - an absolute necessity if you intend to host an instance accessible from the public internet - you need to add a reverse proxy to your setup.
diff --git a/docker-compose.yml b/docker-compose.yml
index 7f55e8b7d5..826a239ee5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,8 +3,7 @@ version: "3"
services:
web:
image: docker.io/thatonecalculator/calckey
- build: .
- restart: always
+ restart: unless-stopped
depends_on:
- db
- redis
@@ -12,39 +11,45 @@ services:
ports:
- "3000:3000"
networks:
- - network
+ - calcnet
+# - web
+ environment:
+ NODE_ENV: production
volumes:
- ./files:/calckey/files
- - ./.config:/calckey/.config:ro
+ - ./config:/calckey/.config:ro
redis:
- restart: always
- image: docker.io/redis:4.0-alpine
+ restart: unless-stopped
+ image: docker.io/redis:7.0-alpine
networks:
- - network
+ - calcnet
volumes:
- ./redis:/data
db:
- restart: always
+ restart: unless-stopped
image: docker.io/postgres:12.2-alpine
networks:
- - network
+ - calcnet
env_file:
- - .config/docker.env
+ - config/docker.env
volumes:
- ./db:/var/lib/postgresql/data
# es:
-# restart: always
-# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
-# environment:
+# restart: unless-stopped
+# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
+# environment:
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
# - "TAKE_FILE_OWNERSHIP=111"
# networks:
-# - network
+# - calcnet
# volumes:
# - ./elasticsearch:/usr/share/elasticsearch/data
networks:
- network:
+ calcnet:
+ # web:
+ # external:
+ # name: web
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index d7920b361e..18e96ff761 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -89,7 +89,7 @@ privacy: "Privatsphäre"
makeFollowManuallyApprove: "Follow-Anfragen benötigen Bestätigung"
defaultNoteVisibility: "Standardsichtbarkeit"
follow: "Folgen"
-followRequest: "Follow-Anfrage senden"
+followRequest: "Follow anfragen"
followRequests: "Follow-Anfragen"
unfollow: "Nicht mehr folgen"
followRequestPending: "Follow-Anfrage ausstehend"
@@ -112,6 +112,7 @@ reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drück
rememberNoteVisibility: "Notizsichtbarkeit merken"
attachCancel: "Anhang entfernen"
markAsSensitive: "Als NSFW markieren"
+accountMoved: "Benutzer hat zu einem anderen Account gewechselt."
unmarkAsSensitive: "Als nicht NSFW markieren"
enterFileName: "Dateinamen eingeben"
mute: "Stummschalten"
@@ -256,7 +257,7 @@ agreeTo: "Ich stimme {0} zu"
tos: "Nutzungsbedingungen"
start: "Anfangen"
home: "Startseite"
-remoteUserCaution: "Diese Informationen sind möglicherweise unvollständig, da der Benutzer von einer fremden Instanz stammt."
+remoteUserCaution: "Informationen von fremden Instanzen sind möglicherweise unvollständig."
activity: "Aktivität"
images: "Bilder"
birthday: "Geburtstag"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 0f3fff72e3..1fff0c22ec 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -59,7 +59,7 @@ followRequestAccepted: "Follow request accepted"
mention: "Mention"
mentions: "Mentions"
directNotes: "Direct notes"
-importAndExport: "Import / Export"
+importAndExport: "Import/Export Data"
import: "Import"
export: "Export"
files: "Files"
@@ -89,7 +89,7 @@ privacy: "Privacy"
makeFollowManuallyApprove: "Follow requests require approval"
defaultNoteVisibility: "Default visibility"
follow: "Follow"
-followRequest: "Send follow request"
+followRequest: "Follow"
followRequests: "Follow requests"
unfollow: "Unfollow"
followRequestPending: "Follow request pending"
@@ -149,6 +149,7 @@ addAccount: "Add account"
loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance"
general: "General"
+accountMoved: "User has moved to a new account:"
wallpaper: "Wallpaper"
setWallpaper: "Set wallpaper"
removeWallpaper: "Remove wallpaper"
@@ -257,7 +258,7 @@ agreeTo: "I agree to {0}"
tos: "Terms of Service"
start: "Begin"
home: "Home"
-remoteUserCaution: "As this user is from a remote instance, the shown information may be incomplete."
+remoteUserCaution: "Information from remote users may be incomplete."
activity: "Activity"
images: "Images"
birthday: "Birthday"
@@ -920,6 +921,15 @@ swipeOnDesktop: "Allow mobile-style swiping on desktop"
logoImageUrl: "Logo image URL"
showAdminUpdates: "Indicate a new Calckey version is avaliable (admin only)"
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:
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
diff --git a/locales/hr-HR.yml b/locales/hr-HR.yml
deleted file mode 100644
index ed97d539c0..0000000000
--- a/locales/hr-HR.yml
+++ /dev/null
@@ -1 +0,0 @@
----
diff --git a/locales/ht-HT.yml b/locales/ht-HT.yml
deleted file mode 100644
index ed97d539c0..0000000000
--- a/locales/ht-HT.yml
+++ /dev/null
@@ -1 +0,0 @@
----
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index b045f5448f..90820d43a2 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -149,6 +149,7 @@ addAccount: "アカウントを追加"
loginFailed: "ログインに失敗しました"
showOnRemote: "リモートで表示"
general: "全般"
+accountMoved: "このユーザーは新しいアカウントに移行しました"
wallpaper: "壁紙"
setWallpaper: "壁紙を設定"
removeWallpaper: "壁紙を削除"
diff --git a/locales/jbo-EN.yml b/locales/jbo-EN.yml
deleted file mode 100644
index ed97d539c0..0000000000
--- a/locales/jbo-EN.yml
+++ /dev/null
@@ -1 +0,0 @@
----
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index f1fa1ca23f..ead4b4a6c5 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -108,7 +108,7 @@ sensitive: "열람주의"
add: "추가"
reaction: "리액션"
reactionSetting: "선택기에 표시할 리액션"
-reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
+reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
rememberNoteVisibility: "공개 범위를 기억하기"
attachCancel: "첨부 취소"
markAsSensitive: "열람주의로 설정"
diff --git a/locales/si-LK.yml b/locales/si-LK.yml
deleted file mode 100644
index ed97d539c0..0000000000
--- a/locales/si-LK.yml
+++ /dev/null
@@ -1 +0,0 @@
----
diff --git a/package.json b/package.json
index 7e0d154012..e2f0d3953b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "calckey",
- "version": "12.119.0-calc.18",
+ "version": "13.0.0",
"codename": "aqua",
"repository": {
"type": "git",
@@ -14,11 +14,13 @@
],
"private": true,
"scripts": {
+ "rebuild": "yarn clean && yarn build",
"build": "yarn workspaces foreach run build && yarn run gulp",
"start": "yarn workspace backend run start",
"start:test": "yarn workspace backend run start:test",
"init": "yarn migrate",
"migrate": "yarn workspace backend run migrate",
+ "revertmigration": "yarn workspace backend run revertmigration",
"migrateandstart": "yarn migrate && yarn start",
"gulp": "gulp build",
"watch": "yarn dev",
@@ -42,6 +44,7 @@
"@bull-board/api": "^4.6.4",
"@bull-board/ui": "^4.6.4",
"@tensorflow/tfjs": "^3.21.0",
+ "calckey-js": "^0.0.17",
"eslint": "^8.28.0",
"execa": "5.1.1",
"gulp": "4.0.2",
diff --git a/packages/backend/.idea/.gitignore b/packages/backend/.idea/.gitignore
new file mode 100644
index 0000000000..13566b81b0
--- /dev/null
+++ b/packages/backend/.idea/.gitignore
@@ -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
diff --git a/packages/backend/migration/1669288094000-AddMovedToAndKnownAs.js b/packages/backend/migration/1669288094000-AddMovedToAndKnownAs.js
new file mode 100644
index 0000000000..8b3c770acb
--- /dev/null
+++ b/packages/backend/migration/1669288094000-AddMovedToAndKnownAs.js
@@ -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"`);
+ }
+
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 2f7a119849..cb34d142a6 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -7,6 +7,7 @@
"start": "node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.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",
"watch": "node watch.mjs",
"lint": "eslint --quiet \"src/**/*.ts\"",
@@ -41,6 +42,7 @@
"blurhash": "1.1.5",
"bull": "4.10.1",
"cacheable-lookup": "7.0.0",
+ "calckey-js": "^0.0.17",
"cbor": "8.1.0",
"chalk": "5.1.2",
"chalk-template": "0.4.0",
@@ -76,7 +78,6 @@
"koa-views": "7.0.2",
"mfm-js": "0.23.0",
"mime-types": "2.1.35",
- "misskey-js": "0.0.14",
"mocha": "10.1.0",
"multer": "1.4.4-lts.1",
"nested-property": "4.0.0",
diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts
index aff67ffa67..9872b0eeb3 100644
--- a/packages/backend/src/config/types.ts
+++ b/packages/backend/src/config/types.ts
@@ -67,6 +67,7 @@ export type Source = {
// Managed hosting stuff
maxUserSignups?: number;
isManagedHosting?: boolean;
+ maxNoteLength?: number;
deepl: {
managed?: boolean;
authKey?: string;
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index 17fdb45ec0..172edca0df 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -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 SEC = 1000;
diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts
index e855ac28ee..3e74118f0f 100644
--- a/packages/backend/src/misc/fetch-meta.ts
+++ b/packages/backend/src/misc/fetch-meta.ts
@@ -7,7 +7,7 @@ export async function fetchMeta(noCache = false): Promise
{
if (!noCache && cache) return cache;
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, {
order: {
id: 'DESC',
@@ -20,7 +20,7 @@ export async function fetchMeta(noCache = false): Promise
{
cache = meta;
return meta;
} 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
.upsert(
Meta,
diff --git a/packages/backend/src/misc/skipped-instances.ts b/packages/backend/src/misc/skipped-instances.ts
index 36367b2e56..a89ca2e3fb 100644
--- a/packages/backend/src/misc/skipped-instances.ts
+++ b/packages/backend/src/misc/skipped-instances.ts
@@ -33,7 +33,6 @@ export async function skippedInstances(hosts: Array
): Array { qb
.where('instance.isSuspended')
- .orWhere('instance.lastCommunicatedAt < :deadTime', { deadTime });
}))
.select('host')
.getRawMany()
diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts
index bc9446be41..3e406bdd7e 100644
--- a/packages/backend/src/models/entities/user.ts
+++ b/packages/backend/src/models/entities/user.ts
@@ -68,6 +68,19 @@ export class User {
})
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', {
default: 0,
comment: 'The count of notes.',
diff --git a/packages/backend/src/models/repositories/page-like.ts b/packages/backend/src/models/repositories/page-like.ts
index 87d6accc34..3f259f9819 100644
--- a/packages/backend/src/models/repositories/page-like.ts
+++ b/packages/backend/src/models/repositories/page-like.ts
@@ -1,12 +1,12 @@
import { db } from '@/db/postgre.js';
import { PageLike } from '@/models/entities/page-like.js';
+import type { User } from '@/models/entities/user.js';
import { Pages } from '../index.js';
-import { User } from '@/models/entities/user.js';
export const PageLikeRepository = db.getRepository(PageLike).extend({
async pack(
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 });
@@ -17,8 +17,8 @@ export const PageLikeRepository = db.getRepository(PageLike).extend({
},
packMany(
- likes: any[],
- me: { id: User['id'] }
+ likes: PageLike[],
+ me: { id: User['id'] },
) {
return Promise.all(likes.map(x => this.pack(x, me)));
},
diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts
index 65b54f8b87..1a8bc50e2e 100644
--- a/packages/backend/src/models/repositories/page.ts
+++ b/packages/backend/src/models/repositories/page.ts
@@ -1,9 +1,9 @@
import { db } from '@/db/postgre.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 { DriveFile } from '@/models/entities/drive-file.js';
-import { User } from '@/models/entities/user.js';
+import type { DriveFile } from '@/models/entities/drive-file.js';
+import type { User } from '@/models/entities/user.js';
import { Users, DriveFiles, PageLikes } from '../index.js';
export const PageRepository = db.getRepository(Page).extend({
diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts
index 5c46ae27a3..138929c4ba 100644
--- a/packages/backend/src/models/repositories/user.ts
+++ b/packages/backend/src/models/repositories/user.ts
@@ -1,22 +1,49 @@
-import { EntityRepository, Repository, In, Not } from 'typeorm';
+import { URL } from 'url';
+import { In, Not } from 'typeorm';
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 { Packed } from '@/misc/schema.js';
-import { awaitAll, Promiseable } from '@/prelude/await-all.js';
+import type { Packed } from '@/misc/schema.js';
+import type { Promiseable } from '@/prelude/await-all.js';
+import { awaitAll } from '@/prelude/await-all.js';
import { populateEmojis } from '@/misc/populate-emojis.js';
import { getAntennas } from '@/misc/antenna-cache.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { db } from '@/db/postgre.js';
-import { Instance } from '../entities/instance.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 { isActor, getApId } from '@/remote/activitypub/type.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(1000 * 60 * 60 * 3);
type IsUserDetailed = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed =
- Detailed extends true ?
+ Detailed extends true ?
ExpectsMe extends true ? Packed<'MeDetailed'> :
ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
Packed<'UserDetailed'> :
@@ -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: 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 {
return user.host == null;
}
function isRemoteUser(user: User): user is IRemoteUser;
function isRemoteUser(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 {
return !isLocalUser(user);
}
@@ -156,6 +195,27 @@ export const UserRepository = db.getRepository(User).extend({
return count > 0;
},
+ async userFromURI(uri: string): Promise {
+ 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 {
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
@@ -320,6 +380,8 @@ export const UserRepository = db.getRepository(User).extend({
...(opts.detail ? {
url: profile!.url,
uri: user.uri,
+ movedToUri: user.movedToUri ? await this.userFromURI(user.movedToUri) : null,
+ alsoKnownAs: user.alsoKnownAs,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts
index 218d861e5b..b70210a0f5 100644
--- a/packages/backend/src/models/schema/user.ts
+++ b/packages/backend/src/models/schema/user.ts
@@ -96,6 +96,16 @@ export const packedUserDetailedNotMeOnlySchema = {
format: 'uri',
nullable: true, optional: false,
},
+ movedToUri: {
+ type: 'string',
+ format: 'uri',
+ nullable: true, optional: false,
+ },
+ alsoKnownAs: {
+ type: 'array',
+ format: 'uri',
+ nullable: true, optional: false,
+ },
createdAt: {
type: 'string',
nullable: false, optional: false,
diff --git a/packages/backend/src/queue/processors/inbox.ts b/packages/backend/src/queue/processors/inbox.ts
index 420d74bb0b..33949672cd 100644
--- a/packages/backend/src/queue/processors/inbox.ts
+++ b/packages/backend/src/queue/processors/inbox.ts
@@ -20,7 +20,7 @@ import { UserPublickey } from '@/models/entities/user-publickey.js';
const logger = new Logger('inbox');
-// ユーザーのinboxにアクティビティが届いた時の処理
+// Processing when an activity arrives in the user's inbox
export default async (job: Bull.Job): Promise => {
const signature = job.data.signature; // HTTP-signature
const activity = job.data.activity;
@@ -30,16 +30,15 @@ export default async (job: Bull.Job): Promise => {
delete info['@context'];
logger.debug(JSON.stringify(info, null, 2));
//#endregion
-
const host = toPuny(new URL(signature.keyId).hostname);
- // ブロックしてたら中断
+ // interrupt if blocked
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
return `Blocked request: ${host}`;
}
- // 非公開モードなら許可なインスタンスのみ
+ // only whitelisted instances in private mode
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
return `Blocked request: ${host}`;
}
@@ -51,7 +50,7 @@ export default async (job: Bull.Job): Promise => {
const dbResolver = new DbResolver();
- // HTTP-Signature keyIdを元にDBから取得
+ // HTTP-Signature keyId from DB
let authUser: {
user: CacheableRemoteUser;
key: UserPublickey | null;
@@ -62,7 +61,7 @@ export default async (job: Bull.Job): Promise => {
try {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
} catch (e) {
- // 対象が4xxならスキップ
+ // Skip if target is 4xx
if (e instanceof StatusError) {
if (e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
diff --git a/packages/backend/src/remote/activitypub/kernel/block/index.ts b/packages/backend/src/remote/activitypub/kernel/block/index.ts
index 5e230ad7b7..c8b60f7b9f 100644
--- a/packages/backend/src/remote/activitypub/kernel/block/index.ts
+++ b/packages/backend/src/remote/activitypub/kernel/block/index.ts
@@ -5,7 +5,7 @@ import DbResolver from '../../db-resolver.js';
import { Users } from '@/models/index.js';
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => {
- // ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
+ // ※ There is a block target in activity.object, which should be a local user that exists.
const dbResolver = new DbResolver();
const blockee = await dbResolver.getUserFromApId(activity.object);
@@ -15,7 +15,7 @@ export default async (actor: CacheableRemoteUser, activity: IBlock): Promise => {
+ // ※ 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(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';
+};
diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts
index 5ef04588e9..21ef83af7a 100644
--- a/packages/backend/src/remote/activitypub/models/person.ts
+++ b/packages/backend/src/remote/activitypub/models/person.ts
@@ -172,6 +172,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise {
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 + '/')) {
return;
}
- //#region このサーバーに既に登録されているか
+ //#region Already registered on this server?
const exist = await Users.findOneBy({ uri }) as IRemoteUser;
if (exist == null) {
@@ -307,7 +309,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
logger.info(`Updating the Person: ${person.id}`);
- // アバターとヘッダー画像をフェッチ
+ // Fetch avatar and header image
const [avatar, banner] = await Promise.all([
person.icon,
person.image,
@@ -317,7 +319,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
: resolveImage(exist, img).catch(() => null),
));
- // カスタム絵文字取得
+ // Custom pictogram acquisition
const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => {
logger.info(`extractEmojis: ${e}`);
return [] as Emoji[];
@@ -343,6 +345,8 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
isBot: getApType(object) === 'Service',
isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers,
+ movedToUri: person.movedTo,
+ alsoKnownAs: person.alsoKnownAs,
isExplorable: !!person.discoverable,
} as Partial;
@@ -374,10 +378,10 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
publishInternalEvent('remoteUserUpdated', { id: exist.id });
- // ハッシュタグ更新
+ // Hashtag Update
updateUsertags(exist, tags);
- // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
+ // If the user in question is a follower, followers will also be updated.
await Followings.update({
followerId: exist.id,
}, {
@@ -388,15 +392,15 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
}
/**
- * Personを解決します。
+ * Resolve Person.
*
- * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ * If the target person is registered in Calckey, it returns it;
+ * otherwise, it fetches it from the remote server, registers it in Calckey, and returns it.
*/
export async function resolvePerson(uri: string, resolver?: Resolver): Promise {
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);
if (exist) {
@@ -404,7 +408,7 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise(2);
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)
.map(item => limit(() => resolveNote(item, resolver))));
await db.transaction(async transactionalEntityManager => {
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
- // とりあえずidを別の時間で生成して順番を維持
+ // For now, generate the id at a different time and maintain the order.
let td = 0;
for (const note of featuredNotes.filter(note => note != null)) {
td -= 1000;
diff --git a/packages/backend/src/remote/activitypub/renderer/index.ts b/packages/backend/src/remote/activitypub/renderer/index.ts
index f100b77ce5..2e1fbf1dd3 100644
--- a/packages/backend/src/remote/activitypub/renderer/index.ts
+++ b/packages/backend/src/remote/activitypub/renderer/index.ts
@@ -1,9 +1,9 @@
-import config from '@/config/index.js';
import { v4 as uuid } from 'uuid';
-import { IActivity } from '../type.js';
-import { LdSignature } from '../misc/ld-signature.js';
+import config from '@/config/index.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 => {
if (x == null) return null;
@@ -19,6 +19,7 @@ export const renderActivity = (x: any): IActivity | null => {
{
// as non-standards
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
+ movedToUri: 'as:movedTo',
sensitive: 'as:sensitive',
Hashtag: 'as:Hashtag',
quoteUrl: 'as:quoteUrl',
diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts
index cd2fd74d47..4f73d28a6a 100644
--- a/packages/backend/src/remote/activitypub/renderer/person.ts
+++ b/packages/backend/src/remote/activitypub/renderer/person.ts
@@ -1,16 +1,16 @@
import { URL } from 'node:url';
import * as mfm from 'mfm-js';
-import renderImage from './image.js';
-import renderKey from './key.js';
import config from '@/config/index.js';
-import { 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 type { ILocalUser } from '@/models/entities/user.js';
import { DriveFiles, UserProfiles } from '@/models/index.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) {
const id = `${config.url}/users/${user.id}`;
@@ -72,16 +72,24 @@ export async function renderPerson(user: ILocalUser) {
tag,
manuallyApprovesFollowers: user.isLocked,
discoverable: !!user.isExplorable,
- publicKey: renderKey(user, keypair, `#main-key`),
+ publicKey: renderKey(user, keypair, '#main-key'),
isCat: user.isCat,
attachment: attachment.length ? attachment : undefined,
} as any;
- if (profile?.birthday) {
+ if (user.movedToUri) {
+ person.movedTo = user.movedToUri;
+ }
+
+ if (user.alsoKnownAs) {
+ person.alsoKnownAs = user.alsoKnownAs;
+ }
+
+ if (profile.birthday) {
person['vcard:bday'] = profile.birthday;
}
- if (profile?.location) {
+ if (profile.location) {
person['vcard:Address'] = profile.location;
}
diff --git a/packages/backend/src/remote/activitypub/type.ts b/packages/backend/src/remote/activitypub/type.ts
index de7eb0ed83..aabbd06797 100644
--- a/packages/backend/src/remote/activitypub/type.ts
+++ b/packages/backend/src/remote/activitypub/type.ts
@@ -156,9 +156,11 @@ export interface IActor extends IObject {
name?: string;
preferredUsername?: string;
manuallyApprovesFollowers?: boolean;
+ movedTo?: string;
+ alsoKnownAs?: string[];
discoverable?: boolean;
inbox: string;
- sharedInbox?: string; // 後方互換性のため
+ sharedInbox?: string; // backward compatibility.. ig
publicKey?: {
id: string;
publicKeyPem: string;
@@ -279,6 +281,11 @@ export interface IFlag extends IActivity {
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 isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
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 isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
+export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts
index 6fc6f2c4d3..fe6b65472e 100644
--- a/packages/backend/src/remote/resolve-user.ts
+++ b/packages/backend/src/remote/resolve-user.ts
@@ -1,13 +1,13 @@
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 { 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 { 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');
@@ -49,9 +49,9 @@ export async function resolveUser(username: string, host: string | null): Promis
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) {
- // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
+ // Prevent multiple attempts to connect to unconnected instances, update before each attempt to prevent subsequent similar attempts
await Users.update(user.id, {
lastFetchedAt: new Date(),
});
@@ -67,7 +67,7 @@ export async function resolveUser(username: string, host: string | null): Promis
// validate uri
const uri = new URL(self.href);
if (uri.hostname !== host) {
- throw new Error(`Invalid uri`);
+ throw new Error('Invalid uri');
}
await Users.update({
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index 250a39bf04..7829157562 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -38,6 +38,7 @@ function inbox(ctx: Router.RouterContext) {
return;
}
+ // @ts-ignore
processInbox(ctx.request.body, signature);
ctx.status = 202;
@@ -86,7 +87,7 @@ router.get('/notes/:note', async (ctx, next) => {
return;
}
- // リモートだったらリダイレクト
+ // redirect if remote
if (note.userHost != null) {
if (note.uri == null || isSelfHost(note.userHost)) {
ctx.status = 500;
diff --git a/packages/backend/src/server/activitypub/following.ts b/packages/backend/src/server/activitypub/following.ts
index f843d79f09..26cee819b2 100644
--- a/packages/backend/src/server/activitypub/following.ts
+++ b/packages/backend/src/server/activitypub/following.ts
@@ -61,7 +61,7 @@ export default async (ctx: Router.RouterContext) => {
followerId: user.id,
} as FindOptionsWhere;
- // カーソルが指定されている場合
+ // If a cursor is specified
if (cursor) {
query.id = LessThan(cursor);
}
@@ -73,7 +73,7 @@ export default async (ctx: Router.RouterContext) => {
order: { id: -1 },
});
- // 「次のページ」があるかどうか
+ // Whether there is a "next page" or not
const inStock = followings.length === limit + 1;
if (inStock) followings.pop();
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index d662d53aff..912998376f 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -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_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_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_pull from './endpoints/users/lists/pull.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___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 = [
['admin/meta', ep___admin_meta],
['admin/abuse-user-reports', ep___admin_abuseUserReports],
@@ -488,6 +493,8 @@ const eps = [
['hashtags/trend', ep___hashtags_trend],
['hashtags/users', ep___hashtags_users],
['i', ep___i],
+ ['i/known-as', ep___i_known_as],
+ ['i/move', ep___i_move],
['i/2fa/done', ep___i_2fa_done],
['i/2fa/key-done', ep___i_2fa_keyDone],
['i/2fa/password-less', ep___i_2fa_passwordLess],
@@ -631,6 +638,7 @@ const eps = [
['users/groups/update', ep___users_groups_update],
['users/lists/create', ep___users_lists_create],
['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/pull', ep___users_lists_pull],
['users/lists/push', ep___users_lists_push],
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 7a4923b944..ae85c44cfb 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -62,6 +62,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
+ if(user.movedToUri != null) throw new ApiError(meta.errors.noSuchUserGroup);
let userList;
let userGroupJoining;
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 7426daeec8..b65f5d078d 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -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 | null> {
- // ブロックしてたら中断
+ // Wait if blocked.
const fetchedMeta = await fetchMeta();
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;
- // リモートから一旦オブジェクトフェッチ
+ // fetching Object once from remote
const resolver = new Resolver();
const object = await resolver.resolve(uri) as any;
- // /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
- // これはDBに存在する可能性があるため再度DB検索
+ // /@user If a URI other than the id is specified,
+ // the URI is determined here
if (uri !== object.id) {
local = await mergePack(me, ...await Promise.all([
dbResolver.getUserFromApId(object.id),
diff --git a/packages/backend/src/server/api/endpoints/app/create.ts b/packages/backend/src/server/api/endpoints/app/create.ts
index a0a7350822..15b2089e7e 100644
--- a/packages/backend/src/server/api/endpoints/app/create.ts
+++ b/packages/backend/src/server/api/endpoints/app/create.ts
@@ -31,6 +31,10 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
+ if(user && user.movedToUri != null) return await Apps.pack("", null, {
+ detail: true,
+ includeSecret: true,
+ });;
// Generate secret
const secret = secureRndstr(32, true);
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 84740bc928..41750f13e1 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -6,7 +6,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
export const meta = {
tags: ['federation'],
- requireCredential: true,
+ requireCredential: false,
requireCredentialPrivateMode: true,
res: {
diff --git a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
index b4edb5f73d..2ecd47f1b8 100644
--- a/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
+++ b/packages/backend/src/server/api/endpoints/i/gallery/posts.ts
@@ -1,5 +1,5 @@
-import define from '../../../define.js';
import { GalleryPosts } from '@/models/index.js';
+import define from '../../../define.js';
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
export const meta = {
@@ -33,7 +33,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
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
.take(ps.limit)
diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts
new file mode 100644
index 0000000000..a7a32aee6f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/known-as.ts
@@ -0,0 +1,102 @@
+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",
+ },
+ uriNull: {
+ message: "User ActivityPup URI is null.",
+ code: "URI_NULL",
+ id: "bf326f31-d430-4f97-9933-5d61e4d48a23",
+ },
+ },
+} 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;
+ const updates = {} as Partial;
+
+ if (!unfiltered) {
+ updates.alsoKnownAs = null;
+ } else {
+ if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
+ if (unfiltered.startsWith("@")) unfiltered = unfiltered.substring(1);
+ if (!unfiltered.includes("@")) throw new ApiError(meta.errors.notRemote);
+
+ const userAddress: string[] = unfiltered.split("@");
+ const knownAs = await resolveUser(userAddress[0], userAddress[1]).catch(
+ (e) => {
+ apiLogger.warn(`failed to resolve remote user: ${e}`);
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+ );
+
+ let toUrl: string | null = knownAs.uri;
+ if (!toUrl) {
+ throw new ApiError(meta.errors.uriNull);
+ }
+ 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(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;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts
new file mode 100644
index 0000000000..d22b178b3b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/move.ts
@@ -0,0 +1,169 @@
+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: 5,
+ },
+
+ 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',
+ },
+ uriNull: {
+ message: "User ActivityPup URI is null.",
+ code: "URI_NULL",
+ id: "bf326f31-d430-4f97-9933-5d61e4d48a23",
+ },
+ localUriNull: {
+ message: "Local User ActivityPup URI is null.",
+ code: "URI_NULL",
+ id: "95ba11b9-90e8-43a5-ba16-7acc1ab32e71",
+ },
+ alreadyMoved: {
+ message: "Account was already moved to another account.",
+ code: "ALREADY_MOVED",
+ id: "b234a14e-9ebe-4581-8000-074b3c215962",
+ },
+ },
+} 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);
+ if (user.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
+
+ let unfiltered: string = ps.moveToAccount;
+ if (!unfiltered) {
+ throw new ApiError(meta.errors.noSuchMoveTarget);
+ }
+
+ if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
+ 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);
+ });
+ let fromUrl: string | null = user.uri;
+ if(!fromUrl) {
+ fromUrl = `${config.url}/users/${user.id}`;
+ }
+
+ let toUrl: string | null = moveTo.uri;
+ if(!toUrl) {
+ throw new ApiError(meta.errors.uriNull);
+ }
+
+ 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;
+
+ if (!toUrl) toUrl = '';
+ updates.movedToUri = toUrl;
+
+ await Users.update(user.id, updates);
+ const iObj = await Users.pack(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;
+});
diff --git a/packages/backend/src/server/api/endpoints/i/page-likes.ts b/packages/backend/src/server/api/endpoints/i/page-likes.ts
index 71e326e2f0..9873872375 100644
--- a/packages/backend/src/server/api/endpoints/i/page-likes.ts
+++ b/packages/backend/src/server/api/endpoints/i/page-likes.ts
@@ -1,5 +1,5 @@
-import define from '../../define.js';
import { PageLikes } from '@/models/index.js';
+import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
export const meta = {
@@ -26,7 +26,7 @@ export const meta = {
ref: 'Page',
},
},
- }
+ },
},
} as const;
@@ -43,7 +43,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
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');
const likes = await query
diff --git a/packages/backend/src/server/api/endpoints/i/pages.ts b/packages/backend/src/server/api/endpoints/i/pages.ts
index f28aed3fd4..7e1820d458 100644
--- a/packages/backend/src/server/api/endpoints/i/pages.ts
+++ b/packages/backend/src/server/api/endpoints/i/pages.ts
@@ -1,5 +1,5 @@
-import define from '../../define.js';
import { Pages } from '@/models/index.js';
+import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
export const meta = {
@@ -33,7 +33,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
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
.take(ps.limit)
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 82540f96bb..1dc8b42b24 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -78,6 +78,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
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;
@@ -163,6 +169,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
+ if(user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
let visibleUsers: User[] = [];
if (ps.visibleUserIds) {
visibleUsers = await Users.findBy({
@@ -250,7 +257,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
}
- // 投稿を作成
+ // Create a post
const note = await create(user, {
createdAt: new Date(),
files: files,
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
index b5c0c9d179..4c767b4890 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts
@@ -28,6 +28,11 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
},
+ accountLocked: {
+ message: 'You migrated. Your account is now locked.',
+ code: 'ACCOUNT_LOCKED',
+ id: 'd390d7e1-8a5e-46ed-b625-06271cafd3d3',
+ },
},
} as const;
@@ -42,6 +47,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
+ if(user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
const note = await getNote(ps.noteId, user).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
diff --git a/packages/backend/src/server/api/endpoints/users/lists/delete-all.ts b/packages/backend/src/server/api/endpoints/users/lists/delete-all.ts
new file mode 100644
index 0000000000..9bea5c1640
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/users/lists/delete-all.ts
@@ -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);
+ }
+});
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
index f31de2b7f4..ae236ce347 100644
--- a/packages/backend/src/server/index.ts
+++ b/packages/backend/src/server/index.ts
@@ -19,7 +19,7 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { createTemp } from '@/misc/create-temp.js';
import { publishMainStream } from '@/services/stream.js';
import * as Acct from '@/misc/acct.js';
-import { envOption } from '../env.js';
+import { envOption } from '@/env.js';
import activityPub from './activitypub.js';
import nodeinfo from './nodeinfo.js';
import wellKnown from './well-known.js';
@@ -164,5 +164,6 @@ export default () => new Promise(resolve => {
}
});
+ // @ts-ignore
server.listen(config.port, resolve);
});
diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts
index b4216d9d92..0bb9e05c53 100644
--- a/packages/backend/src/server/nodeinfo.ts
+++ b/packages/backend/src/server/nodeinfo.ts
@@ -11,11 +11,11 @@ const router = new Router();
const nodeinfo2_1path = '/nodeinfo/2.1';
const nodeinfo2_0path = '/nodeinfo/2.0';
-export const links = [/* (awaiting release) {
- rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+export const links = [{
+ rel: 'https://nodeinfo.diaspora.software/ns/schema/2.1',
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,
}];
@@ -96,6 +96,7 @@ router.get(nodeinfo2_1path, async ctx => {
router.get(nodeinfo2_0path, async ctx => {
const base = await cache.fetch(null, () => nodeinfo2());
+ // @ts-ignore
delete base.software.repository;
ctx.body = { version: '2.0', ...base };
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index b6c8627433..0342603c6f 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -22,37 +22,32 @@
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
- const v = localStorage.getItem('v') || VERSION;
-
//#region Detect language & fetch translations
- const localeVersion = localStorage.getItem('localeVersion');
- const localeOutdated = (localeVersion == null || localeVersion !== v);
-
- if (!localStorage.hasOwnProperty('locale') || localeOutdated) {
- const supportedLangs = LANGS;
- let lang = localStorage.getItem('lang');
- if (lang == null || !supportedLangs.includes(lang)) {
- if (supportedLangs.includes(navigator.language)) {
- lang = navigator.language;
- } else {
- lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
-
- // Fallback
- if (lang == null) lang = 'en-US';
- }
- }
-
- const res = await fetch(`/assets/locales/${lang}.${v}.json`);
- if (res.status === 200) {
- localStorage.setItem('lang', lang);
- localStorage.setItem('locale', await res.text());
- localStorage.setItem('localeVersion', v);
+ const v = localStorage.getItem('v') || VERSION;
+
+ const supportedLangs = LANGS;
+ let lang = localStorage.getItem('lang');
+ if (lang == null || !supportedLangs.includes(lang)) {
+ if (supportedLangs.includes(navigator.language)) {
+ lang = navigator.language;
} else {
- await checkUpdate();
- renderError('LOCALE_FETCH');
- return;
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+ // Fallback
+ if (lang == null) lang = 'en-US';
}
}
+
+ const res = await fetch(`/assets/locales/${lang}.${v}.json`);
+ if (res.status === 200) {
+ localStorage.setItem('lang', lang);
+ localStorage.setItem('locale', await res.text());
+ localStorage.setItem('localeVersion', v);
+ } else {
+ await checkUpdate();
+ renderError('LOCALE_FETCH');
+ return;
+ }
//#endregion
//#region Script
diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts
index 8ba2963d5e..fa8ba6bf1b 100644
--- a/packages/backend/src/services/note/create.ts
+++ b/packages/backend/src/services/note/create.ts
@@ -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(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.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) {
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.localOnly = true;
- // サイレンス
+ // enforce silent clients on server
if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
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) {
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') {
data.visibility = 'home';
}
- // Renote対象がfollowersならfollowersにする
+ // If the target of Renote is followers, make it followers.
if (data.renote && data.renote.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') {
data.visibility = 'home';
}
- // ローカルのみをRenoteしたらローカルのみにする
+ // Renote local only if you Renote local only.
if (data.renote && data.renote.localOnly && data.channel == null) {
data.localOnly = true;
}
- // ローカルのみにリプライしたらローカルのみにする
+ // If you reply to local only, make it local only.
if (data.reply && data.reply.localOnly && data.channel == null) {
data.localOnly = true;
}
diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts
index 245cf858d8..6833e6eb58 100644
--- a/packages/backend/test/utils.ts
+++ b/packages/backend/test/utils.ts
@@ -6,7 +6,7 @@ import * as childProcess from 'child_process';
import * as http from 'node:http';
import { SIGKILL } from 'constants';
import WebSocket from 'ws';
-import * as misskey from 'misskey-js';
+import * as misskey from 'calckey-js';
import fetch from 'node-fetch';
import FormData from 'form-data';
import { DataSource } from 'typeorm';
diff --git a/packages/client/package.json b/packages/client/package.json
index bc49ac0211..d8d57dddeb 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -15,10 +15,11 @@
"@vitejs/plugin-vue": "3.2.0",
"@vue/compiler-sfc": "3.2.45",
"autobind-decorator": "2.4.0",
- "autosize": "5.0.1",
+ "autosize": "5.0.2",
"blurhash": "1.1.5",
"broadcast-channel": "4.18.1",
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c",
+ "calckey-js": "^0.0.17",
"chart.js": "4.0.1",
"chartjs-adapter-date-fns": "2.0.1",
"chartjs-plugin-gradient": "0.5.1",
@@ -34,7 +35,6 @@
"katex": "0.16.3",
"matter-js": "0.18.0",
"mfm-js": "0.23.0",
- "misskey-js": "0.0.14",
"photoswipe": "5.3.3",
"prismjs": "1.29.0",
"punycode": "2.1.1",
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index eefcc9a100..62fc2d82f1 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -1,5 +1,5 @@
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 { i18n } from './i18n';
import { del, get, set } from '@/scripts/idb-proxy';
diff --git a/packages/client/src/components/MkAbuseReportWindow.vue b/packages/client/src/components/MkAbuseReportWindow.vue
index c83592d8c1..b8ef6686c0 100644
--- a/packages/client/src/components/MkAbuseReportWindow.vue
+++ b/packages/client/src/components/MkAbuseReportWindow.vue
@@ -24,7 +24,7 @@
diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue
index 6fd5ccb1cf..397b0406bb 100644
--- a/packages/client/src/components/MkFollowButton.vue
+++ b/packages/client/src/components/MkFollowButton.vue
@@ -31,7 +31,7 @@
diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue
index f6e2f4eaa7..b4ff5bdc35 100644
--- a/packages/client/src/components/MkInstanceCardMini.vue
+++ b/packages/client/src/components/MkInstanceCardMini.vue
@@ -10,7 +10,7 @@
+
+
diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue
index 613203091b..e350c48dad 100644
--- a/packages/client/src/components/MkNote.vue
+++ b/packages/client/src/components/MkNote.vue
@@ -107,7 +107,7 @@
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as mfm from 'mfm-js';
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 XNoteHeader from '@/components/MkNoteHeader.vue';
import XNoteSimple from '@/components/MkNoteSimple.vue';
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index 2deb184416..cfa56a019d 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -117,7 +117,7 @@
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index 85baafa35a..2c019d45ac 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -11,7 +11,7 @@