Merge pull request 'Add account migration' (#9186) from account_migration into develop
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9186
This commit is contained in:
commit
c03b4f8cfb
47 changed files with 669 additions and 119 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -59,3 +59,8 @@ packages/backend/assets/instance.css
|
|||
*.blend3
|
||||
*.blend4
|
||||
*.blend5
|
||||
|
||||
#intelij stuff
|
||||
packages/backend/.idea/backend.iml
|
||||
packages/backend/.idea/modules.xml
|
||||
packages/backend/.idea/vcs.xml
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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. 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."
|
||||
|
||||
_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."
|
||||
|
|
|
@ -149,6 +149,7 @@ addAccount: "アカウントを追加"
|
|||
loginFailed: "ログインに失敗しました"
|
||||
showOnRemote: "リモートで表示"
|
||||
general: "全般"
|
||||
accountMoved: "このユーザーは新しいアカウントに移行しました"
|
||||
wallpaper: "壁紙"
|
||||
setWallpaper: "壁紙を設定"
|
||||
removeWallpaper: "壁紙を削除"
|
||||
|
|
|
@ -108,7 +108,7 @@ sensitive: "열람주의"
|
|||
add: "추가"
|
||||
reaction: "리액션"
|
||||
reactionSetting: "선택기에 표시할 리액션"
|
||||
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
|
||||
reactionSettingDescription2: "끌어서 순서 변경, 클릭해서 삭제, +를 눌러서 추가할 수 있습니다."
|
||||
rememberNoteVisibility: "공개 범위를 기억하기"
|
||||
attachCancel: "첨부 취소"
|
||||
markAsSensitive: "열람주의로 설정"
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"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",
|
||||
|
|
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: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\"",
|
||||
|
|
|
@ -7,7 +7,7 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
|
|||
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<Meta> {
|
|||
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,
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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<Instance | null>(1000 * 60 * 60 * 3);
|
||||
|
||||
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
|
||||
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
|
||||
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<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 {
|
||||
return user.host == null;
|
||||
}
|
||||
|
||||
function isRemoteUser(user: User): user is IRemoteUser;
|
||||
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 {
|
||||
return !isLocalUser(user);
|
||||
}
|
||||
|
@ -156,6 +195,27 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
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> {
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<InboxJobData>): Promise<string> => {
|
||||
const signature = job.data.signature; // HTTP-signature
|
||||
const activity = job.data.activity;
|
||||
|
@ -30,16 +30,15 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
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<InboxJobData>): Promise<string> => {
|
|||
|
||||
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<InboxJobData>): Promise<string> => {
|
|||
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}`;
|
||||
|
|
|
@ -5,7 +5,7 @@ import DbResolver from '../../db-resolver.js';
|
|||
import { Users } from '@/models/index.js';
|
||||
|
||||
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 blockee = await dbResolver.getUserFromApId(activity.object);
|
||||
|
@ -15,7 +15,7 @@ export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<str
|
|||
}
|
||||
|
||||
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 }));
|
||||
|
|
|
@ -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 { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import type { 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 performDeleteActivity from './delete/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 block from './block/index.js';
|
||||
import flag from './flag/index.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import Resolver from '../resolver.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import move from './move/index.js';
|
||||
import type { IObject } from '../type.js';
|
||||
|
||||
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
|
@ -68,6 +87,8 @@ async function performOneActivity(actor: CacheableRemoteUser, activity: IObject)
|
|||
await block(actor, activity);
|
||||
} else if (isFlag(activity)) {
|
||||
await flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
await move(actor,activity);
|
||||
} else {
|
||||
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||
}
|
||||
|
|
85
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
85
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
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,
|
||||
});
|
||||
|
||||
//TODO remove this
|
||||
console.log(followings);
|
||||
|
||||
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) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') return meta.errors.blocking;
|
||||
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') return meta.errors.blocked;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return 'ok';
|
||||
};
|
|
@ -172,6 +172,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
|||
lastFetchedAt: new Date(),
|
||||
name: truncate(person.name, nameLength),
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
movedToUri: person.movedTo,
|
||||
alsoKnownAs: person.alsoKnownAs,
|
||||
isExplorable: !!person.discoverable,
|
||||
username: person.preferredUsername,
|
||||
usernameLower: person.preferredUsername!.toLowerCase(),
|
||||
|
@ -277,21 +279,21 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
|||
}
|
||||
|
||||
/**
|
||||
* Personの情報を更新します。
|
||||
* Misskeyに対象のPersonが登録されていなければ無視します。
|
||||
* Update Person data from remote.
|
||||
* If the target Person is not registered in Calckey, it is ignored.
|
||||
* @param uri URI of Person
|
||||
* @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> {
|
||||
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<User>;
|
||||
|
||||
|
@ -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<CacheableUser> {
|
||||
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<C
|
|||
}
|
||||
//#endregion
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
// Fetched from remote server and registered
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
return await createPerson(uri, resolver);
|
||||
}
|
||||
|
@ -482,14 +486,14 @@ export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
|
|||
// Resolve and regist Notes
|
||||
const limit = promiseLimit<Note | null>(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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`;
|
||||
|
@ -71,17 +71,18 @@ export async function renderPerson(user: ILocalUser) {
|
|||
image: banner ? renderImage(banner) : null,
|
||||
tag,
|
||||
manuallyApprovesFollowers: user.isLocked,
|
||||
movedToUri: user.movedToUri,
|
||||
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 (profile.birthday) {
|
||||
person['vcard:bday'] = profile.birthday;
|
||||
}
|
||||
|
||||
if (profile?.location) {
|
||||
if (profile.location) {
|
||||
person['vcard:Address'] = profile.location;
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -61,7 +61,7 @@ export default async (ctx: Router.RouterContext) => {
|
|||
followerId: user.id,
|
||||
} as FindOptionsWhere<Following>;
|
||||
|
||||
// カーソルが指定されている場合
|
||||
// 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();
|
||||
|
||||
|
|
|
@ -325,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],
|
||||
|
@ -489,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],
|
||||
|
|
|
@ -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> {
|
||||
// ブロックしてたら中断
|
||||
// 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),
|
||||
|
|
|
@ -6,7 +6,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireCredential: true,
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
|
||||
res: {
|
||||
|
|
85
packages/backend/src/server/api/endpoints/i/known-as.ts
Normal file
85
packages/backend/src/server/api/endpoints/i/known-as.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { In } from 'typeorm';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js';
|
||||
import { resolveUser } from '@/remote/resolve-user.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import acceptAllFollowRequests from '@/services/following/requests/accept-all.js';
|
||||
import { publishToFollowers } from '@/services/i/update.js';
|
||||
import { apiLogger } from '../../logger.js';
|
||||
import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||
import define from '../../define.js';
|
||||
import { DAY } from '@/const.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 not remote.',
|
||||
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);
|
||||
|
||||
let userAddress: string[] = unfiltered.split("@");
|
||||
|
||||
const knownAs: User = await resolveUser(userAddress[0], userAddress[1]).catch(e => {
|
||||
apiLogger.warn(`failed to resolve remote user: ${e}`);
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
});
|
||||
|
||||
const updates = {} as Partial<User>;
|
||||
|
||||
if(!knownAs.uri) knownAs.uri = "";
|
||||
updates.alsoKnownAs = [knownAs.uri];
|
||||
|
||||
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;
|
||||
});
|
97
packages/backend/src/server/api/endpoints/i/move.ts
Normal file
97
packages/backend/src/server/api/endpoints/i/move.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
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 type { IActivity } from '@/remote/activitypub/type.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { apiLogger } from '../../logger.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: DAY,
|
||||
max: 1,
|
||||
},
|
||||
|
||||
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.',
|
||||
code: 'REMOTE_ACCOUNT_FORBIDS',
|
||||
id: 'b5c90186-4ab0-49c8-9bba-a1f766282ba4',
|
||||
},
|
||||
notRemote: {
|
||||
message: 'User not remote.',
|
||||
code: 'NOT_REMOTE',
|
||||
id: '4362f8dc-731f-4ad8-a694-be2a88922a24',
|
||||
},
|
||||
adminForbidden: {
|
||||
message: 'Adminds cant migrate.',
|
||||
code: 'NOT_ADMIN_FORBIDDEN',
|
||||
id: '4362e8dc-731f-4ad8-a694-be2a88922a24',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
moveToAccount: { type: 'string' },
|
||||
},
|
||||
required: ['moveToAccount'],
|
||||
} as const;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
let allowed = false;
|
||||
|
||||
moveTo.alsoKnownAs?.forEach(element => {
|
||||
if (user.uri?.includes(element)) allowed = true;
|
||||
});
|
||||
|
||||
if (!allowed || !moveTo.uri || !user.uri) throw new ApiError(meta.errors.remoteAccountForbids);
|
||||
|
||||
(async (): Promise<void> => {
|
||||
const moveAct = await moveActivity(moveTo.uri!, user.uri!);
|
||||
const dm = new DeliverManager(user, moveAct);
|
||||
dm.addFollowersRecipe();
|
||||
dm.execute();
|
||||
})();
|
||||
return true;
|
||||
});
|
||||
|
||||
async function moveActivity(to: string, from: string): Promise<IActivity | null> {
|
||||
const activity = {
|
||||
id: 'foo',
|
||||
actor: from,
|
||||
type: 'Move',
|
||||
object: from,
|
||||
target: to,
|
||||
} as any;
|
||||
|
||||
return renderActivity(activity);
|
||||
}
|
|
@ -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) 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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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) => {
|
||||
// チャンネル外にリプライしたら対象のスコープに合わせる
|
||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
warn?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
|
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>
|
|
@ -12,7 +12,6 @@ defineProps<{
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.jmgmzlwq {
|
||||
font-size: 0.8em;
|
||||
padding: 16px;
|
||||
background: var(--infoWarnBg);
|
||||
color: var(--infoWarnFg);
|
||||
|
|
|
@ -34,8 +34,9 @@
|
|||
<FormSection>
|
||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||
<div class="_formLinks">
|
||||
<FormLink to="/@t1c@i.calckey.cloud"><Mfm :text="'$[sparkle @t1c@i.calckey.cloud] (Main fork developer)'"/></FormLink>
|
||||
<FormLink to="/@syuilo@misskey.io"><Mfm :text="'@syuilo@misskey.io (Misskey developer)'"/></FormLink>
|
||||
<FormLink to="/@t1c@i.calckey.cloud"><Mfm :text="'$[sparkle @t1c@i.calckey.cloud] (Main developer)'"/></FormLink>
|
||||
<FormLink to="/@cleo@tech.lgbt"><Mfm :text="'$[sparkle @cleo@tech.lgbt] (Maintainer)'"/></FormLink>
|
||||
<FormLink to="/@syuilo@misskey.io"><Mfm :text="'@syuilo@misskey.io (Original Misskey developer)'"/></FormLink>
|
||||
<FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink>
|
||||
</div>
|
||||
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
<template><MkStickyContainer>
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.proxyAccount }}</template>
|
||||
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.proxyAccount }}</template>
|
||||
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer></MkStickyContainer>
|
||||
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -9,26 +9,26 @@
|
|||
<div v-if="origin === 'local'">
|
||||
<template v-if="tag == null">
|
||||
<MkFolder class="_gap" persist-key="explore-pinned-users">
|
||||
<template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<template #header><i class="ph-bookmark-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<XUserList :pagination="pinnedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="$i != null" class="_gap" persist-key="explore-popular-users">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<template #header><i class="ph-chart-line-up-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="$i != null" class="_gap" persist-key="explore-recently-updated-users">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<template #header><i class="ph-activity-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsers"/>
|
||||
</MkFolder>
|
||||
<MkFolder v-if="$i != null" class="_gap" persist-key="explore-recently-registered-users">
|
||||
<template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
|
||||
<template #header><i class="ph-butterfly-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsers"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else>
|
||||
<MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
||||
<template #header><i class="ph-hash-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
|
||||
|
||||
<div class="vxjfqztj">
|
||||
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
|
||||
|
@ -37,21 +37,21 @@
|
|||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
|
||||
<template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||
<template #header><i class="ph-hash-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||
<XUserList :pagination="tagUsers"/>
|
||||
</MkFolder>
|
||||
|
||||
<template v-if="tag == null && $i != null">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<template #header><i class="ph-chart-line-up-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
||||
<XUserList :pagination="popularUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<template #header><i class="ph-activity-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
||||
<XUserList :pagination="recentlyUpdatedUsersF"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
||||
<template #header><i class="ph-rocke-launch-bold ph-lg ph-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
||||
<XUserList :pagination="recentlyRegisteredUsersF"/>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
|
|
@ -130,11 +130,11 @@ function onRead(ids): void {
|
|||
function startMenu(ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.messagingWithUser,
|
||||
icon: 'fas fa-user',
|
||||
icon: 'ph-user-bold ph-lg',
|
||||
action: () => { startUser(); },
|
||||
}, {
|
||||
text: i18n.ts.messagingWithGroup,
|
||||
icon: 'fas fa-users',
|
||||
icon: 'ph-users-three-bold ph-lg',
|
||||
action: () => { startGroup(); },
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
|
|
@ -134,6 +134,11 @@ const menuDef = computed(() => [{
|
|||
}, {
|
||||
title: i18n.ts.otherSettings,
|
||||
items: [{
|
||||
icon: 'ph-airplane-takeoff-bold ph-lg',
|
||||
text: i18n.ts.migration,
|
||||
to: '/settings/migration',
|
||||
active: currentPage?.route.name === 'migration',
|
||||
}, {
|
||||
icon: 'ph-package-bold ph-lg',
|
||||
text: i18n.ts.importAndExport,
|
||||
to: '/settings/import-export',
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSection v-if="instance.enableTwitterIntegration">
|
||||
<template #label><i class="fab fa-twitter"></i> Twitter</template>
|
||||
<template #label><i class="ph-twitter-logo-bold ph-lg"></i> Twitter</template>
|
||||
<p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
|
||||
<MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ i18n.ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else primary @click="connectTwitter">{{ i18n.ts.connectService }}</MkButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="instance.enableDiscordIntegration">
|
||||
<template #label><i class="fab fa-discord"></i> Discord</template>
|
||||
<template #label><i class="ph-discord-logo-bold ph-lg"></i> Discord</template>
|
||||
<p v-if="integrations.discord">{{ i18n.ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
|
||||
<MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ i18n.ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else primary @click="connectDiscord">{{ i18n.ts.connectService }}</MkButton>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="instance.enableGithubIntegration">
|
||||
<template #label><i class="fab fa-github"></i> GitHub</template>
|
||||
<template #label><i class="ph-github-logo-bold ph-lg"></i> GitHub</template>
|
||||
<p v-if="integrations.github">{{ i18n.ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
|
||||
<MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ i18n.ts.disconnectService }}</MkButton>
|
||||
<MkButton v-else primary @click="connectGithub">{{ i18n.ts.connectService }}</MkButton>
|
||||
|
|
61
packages/client/src/pages/settings/migration.vue
Normal file
61
packages/client/src/pages/settings/migration.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div class="_formRoot">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.moveTo }}</template>
|
||||
<FormInput v-model="moveToAccount" class="_formBlock">
|
||||
<template #prefix><i class="ph-airplane-takeoff-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.moveToLabel }}</template>
|
||||
</FormInput>
|
||||
<FormButton primary danger @click="move(moveToAccount)">
|
||||
{{ i18n.ts.moveAccount }}
|
||||
</FormButton>
|
||||
<div class="label">{{ i18n.ts.moveAccountDescription }}</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.moveFrom }}</template>
|
||||
<FormInput v-model="accountAlias" class="_formBlock">
|
||||
<template #prefix><i class="ph-airplane-landing-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.moveFromLabel }}</template>
|
||||
</FormInput>
|
||||
<FormButton class="button" inline primary @click="save(accountAlias)">
|
||||
<i class="ph-floppy-disk-back-bold ph-lg"></i> {{ i18n.ts.save }}
|
||||
</FormButton>
|
||||
<div class="label">{{ i18n.ts.moveFromDescription }}</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let moveToAccount = $ref('');
|
||||
let accountAlias = $ref('');
|
||||
|
||||
async function save(account): Promise<void> {
|
||||
os.apiWithDialog('i/known-as', {
|
||||
alsoKnownAs: account,
|
||||
});
|
||||
}
|
||||
|
||||
async function move(account): Promise<void> {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('migrationConfirm', { account: account.toString() }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.api('i/move', {
|
||||
moveToAccount: account,
|
||||
});
|
||||
}
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.security,
|
||||
icon: 'ph-lock-bold ph-lg',
|
||||
});
|
||||
</script>
|
|
@ -9,7 +9,7 @@
|
|||
<template #label>{{ i18n.ts.twoStepAuthentication }}</template>
|
||||
<X2fa/>
|
||||
</FormSection>
|
||||
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.signinHistory }}</template>
|
||||
<MkPagination :pagination="pagination" disable-auto-load>
|
||||
|
@ -52,7 +52,7 @@ const pagination = {
|
|||
limit: 5,
|
||||
};
|
||||
|
||||
async function change() {
|
||||
async function change(): Promise<void> {
|
||||
const { canceled: canceled1, result: currentPassword } = await os.inputText({
|
||||
title: i18n.ts.currentPassword,
|
||||
type: 'password',
|
||||
|
@ -78,14 +78,14 @@ async function change() {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
os.apiWithDialog('i/change-password', {
|
||||
currentPassword,
|
||||
newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
function regenerateToken() {
|
||||
function regenerateToken(): void {
|
||||
os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<!-- <div class="punished" v-if="user.isSilenced"><i class="ph-warning-bold ph-lg" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
||||
|
||||
<div class="profile">
|
||||
<MkMoved v-if="user.movedToUri" :host="user.movedToUri.host" :acct="user.movedToUri.username" />
|
||||
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
|
||||
|
||||
<div :key="user.id" class="_block main">
|
||||
|
@ -120,6 +121,7 @@ import MkFolder from '@/components/MkFolder.vue';
|
|||
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkMoved from '@/components/MkMoved.vue';
|
||||
import { getScrollPosition } from '@/scripts/scroll';
|
||||
import { getUserMenu } from '@/scripts/get-user-menu';
|
||||
import number from '@/filters/number';
|
||||
|
@ -128,6 +130,7 @@ import * as os from '@/os';
|
|||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import { host } from '@/config';
|
||||
|
||||
const XPhotos = defineAsyncComponent(() => import('./index.photos.vue'));
|
||||
const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
|
||||
|
|
|
@ -112,7 +112,7 @@ definePageMetadata(
|
|||
computed(() =>
|
||||
user
|
||||
? {
|
||||
icon: 'fas fa-user',
|
||||
icon: 'ph-user-bold ph-lg',
|
||||
title: user.name
|
||||
? `${user.name} (@${user.username})`
|
||||
: `@${user.username}`,
|
||||
|
|
|
@ -180,6 +180,10 @@ export const routes = [{
|
|||
path: '/preferences-backups',
|
||||
name: 'preferences-backups',
|
||||
component: page(() => import('./pages/settings/preferences-backups.vue')),
|
||||
}, {
|
||||
path: '/migration',
|
||||
name: 'migration',
|
||||
component: page(() => import('./pages/settings/migration.vue')),
|
||||
}, {
|
||||
path: '/custom-css',
|
||||
name: 'general',
|
||||
|
|
Loading…
Reference in a new issue