set alsoKnownAs via /i/update

This commit is contained in:
Namekuji 2023-04-20 22:03:18 -04:00
parent 2a0efffa3e
commit 55f9112eed
9 changed files with 145 additions and 181 deletions

View file

@ -102,32 +102,6 @@ export class AccountMoveService {
return iObj;
}
/**
* Create an alias of an old remote account.
*
* The user's new profile will be published to the followers.
*/
@bindThis
public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
await this.usersRepository.update(me.id, updates);
me = Object.assign(me, updates);
// Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
detail: true,
includeSecrets: true,
});
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
if (me.isLocked === false) {
await this.userFollowingService.acceptAllFollowRequests(me);
}
this.accountUpdateService.publishToFollowers(me.id);
return iObj;
}
@bindThis
public async move(src: User, dst: User): Promise<void> {
// Copy blockings and mutings, and update lists
@ -144,9 +118,9 @@ export class AccountMoveService {
// follow the new account and unfollow the old one
const proxy = await this.proxyAccountService.fetch();
const followings = await this.followingsRepository.findBy({
followeeId: src.id,
followerHost: IsNull(), // follower is local
followerId: proxy ? Not(proxy.id) : undefined,
followeeId: src.id,
followerHost: IsNull(), // follower is local
followerId: proxy ? Not(proxy.id) : undefined,
});
const followJobs = followings.map(following => ({
from: { id: following.followerId },

View file

@ -223,7 +223,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_move from './endpoints/i/move.js';
import * as ep___i_knownAs from './endpoints/i/known-as.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
@ -560,7 +559,6 @@ const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.defau
const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default };
const $i_knownAs: Provider = { provide: 'ep:i/known-as', useClass: ep___i_knownAs.default };
const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
@ -901,7 +899,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_updateEmail,
$i_update,
$i_move,
$i_knownAs,
$i_webhooks_create,
$i_webhooks_list,
$i_webhooks_show,
@ -1236,7 +1233,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_updateEmail,
$i_update,
$i_move,
$i_knownAs,
$i_webhooks_create,
$i_webhooks_list,
$i_webhooks_show,

View file

@ -223,7 +223,6 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_move from './endpoints/i/move.js';
import * as ep___i_knownAs from './endpoints/i/known-as.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
@ -558,7 +557,6 @@ const eps = [
['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update],
['i/move', ep___i_move],
['i/known-as', ep___i_knownAs],
['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show],

View file

@ -1,87 +0,0 @@
import { Injectable } from '@nestjs/common';
import ms from 'ms';
import { User } from '@/models/entities/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '@/server/api/error.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { ApiLoggerService } from '@/server/api/ApiLoggerService.js';
export const meta = {
tags: ['users'],
secure: true,
requireCredential: true,
prohibitMoved: true,
limit: {
duration: ms('1day'),
max: 30,
},
errors: {
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',
},
forbiddenToSetYourself: {
message: 'You can\'t set yourself as your own alias.',
code: 'FORBIDDEN_TO_SET_YOURSELF',
id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
alsoKnownAs: { type: 'string' },
},
required: ['alsoKnownAs'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService,
private accountMoveService: AccountMoveService,
) {
super(meta, paramDef, async (ps, me) => {
let unfiltered = ps.alsoKnownAs;
const updates = {} as Partial<User>;
if (!unfiltered) {
updates.alsoKnownAs = null;
} else {
// Parse user's input into the old account
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.noSuchUser);
const userAddress = unfiltered.split('@');
// Retrieve the old account
const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`);
throw new ApiError(meta.errors.noSuchUser);
});
if (knownAs.id === me.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
const toUrl = this.accountMoveService.getUserUri(knownAs);
if (!toUrl) throw new ApiError(meta.errors.uriNull);
updates.alsoKnownAs = me.alsoKnownAs?.includes(toUrl) ? me.alsoKnownAs : me.alsoKnownAs?.concat([toUrl]) ?? [toUrl];
}
return await this.accountMoveService.createAlias(me, updates);
});
}
}

View file

@ -19,7 +19,10 @@ import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -71,6 +74,24 @@ export const meta = {
code: 'TOO_MANY_MUTED_WORDS',
id: '010665b1-a211-42d2-bc64-8f6609d79785',
},
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',
},
forbiddenToSetYourself: {
message: 'You can\'t set yourself as your own alias.',
code: 'FORBIDDEN_TO_SET_YOURSELF',
id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4',
},
},
res: {
@ -129,6 +150,12 @@ export const paramDef = {
emailNotificationTypes: { type: 'array', items: {
type: 'string',
} },
alsoKnownAs: {
type: 'array',
maxItems: 5,
uniqueItems: true,
items: { type: 'string' },
},
},
} as const;
@ -153,6 +180,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private globalEventService: GlobalEventService,
private userFollowingService: UserFollowingService,
private accountUpdateService: AccountUpdateService,
private accountMoveService: AccountMoveService,
private remoteUserResolveService: RemoteUserResolveService,
private apiLoggerService: ApiLoggerService,
private hashtagService: HashtagService,
private roleService: RoleService,
private cacheService: CacheService,
@ -260,6 +290,41 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
});
}
if (ps.alsoKnownAs) {
if (_user.movedToUri) {
throw new ApiError({
message: 'You have moved your account.',
code: 'YOUR_ACCOUNT_MOVED',
id: '56f20ec9-fd06-4fa5-841b-edd6d7d4fa31',
httpStatusCode: 403,
});
}
// Parse user's input into the old account
const newAlsoKnownAs: string[] = [];
for (const line of ps.alsoKnownAs) {
let unfiltered = line;
if (unfiltered.startsWith('acct:')) unfiltered = unfiltered.substring(5);
if (unfiltered.startsWith('@')) unfiltered = unfiltered.substring(1);
if (!unfiltered.includes('@')) throw new ApiError(meta.errors.noSuchUser);
const userAddress = unfiltered.split('@');
// Retrieve the old account
const knownAs = await this.remoteUserResolveService.resolveUser(userAddress[0], userAddress[1]).catch((e) => {
this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`);
throw new ApiError(meta.errors.noSuchUser);
});
if (knownAs.id === _user.id) throw new ApiError(meta.errors.forbiddenToSetYourself);
const toUrl = this.accountMoveService.getUserUri(knownAs);
if (!toUrl) throw new ApiError(meta.errors.uriNull);
newAlsoKnownAs.push(toUrl);
}
updates.alsoKnownAs = newAlsoKnownAs.length > 0 ? newAlsoKnownAs : null;
}
//#region emojis/tags
let emojis = [] as string[];

View file

@ -48,8 +48,8 @@ describe('Account Move', () => {
}, 1000 * 10);
test('Able to create an alias', async () => {
await api('/i/known-as', {
alsoKnownAs: `@alice@${url.hostname}`,
await api('/i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
const newBob = await Users.findOneByOrFail({ id: bob.id });
@ -58,8 +58,8 @@ describe('Account Move', () => {
});
test('Able to set remote user (but may fail)', async () => {
const res = await api('/i/known-as', {
alsoKnownAs: '@syuilo@example.com',
const res = await api('/i/update', {
alsoKnownAs: ['@syuilo@example.com'],
}, bob);
assert.strictEqual(res.status, 400);
@ -67,22 +67,19 @@ describe('Account Move', () => {
assert.strictEqual(res.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
});
test('Nothing happen when alias duplicated', async () => {
await api('/i/known-as', {
alsoKnownAs: `@alice@${url.hostname}`,
}, bob);
await api('/i/known-as', {
alsoKnownAs: `@alice@${url.hostname}`,
test('Unable to add duplicated aliases to alsoKnownAs', async () => {
const res = await api('/i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@alice@${url.hostname}`],
}, bob);
const newBob = await Users.findOneByOrFail({ id: bob.id });
assert.strictEqual(newBob.alsoKnownAs?.length, 1);
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${alice.id}`);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'INVALID_PARAM');
assert.strictEqual(res.body.error.id, '3d81ceae-475f-4600-b2a8-2bc116157532');
});
test('Unable to add itself', async () => {
const res = await api('/i/known-as', {
alsoKnownAs: `@bob@${url.hostname}`,
const res = await api('/i/update', {
alsoKnownAs: [`@bob@${url.hostname}`],
}, bob);
assert.strictEqual(res.status, 400);
@ -91,8 +88,8 @@ describe('Account Move', () => {
});
test('Unable to add a nonexisting local account to alsoKnownAs', async () => {
const res = await api('/i/known-as', {
alsoKnownAs: `@nonexist@${url.hostname}`,
const res = await api('/i/update', {
alsoKnownAs: [`@nonexist@${url.hostname}`],
}, bob);
assert.strictEqual(res.status, 400);
@ -101,11 +98,8 @@ describe('Account Move', () => {
});
test('Able to add two existing local account to alsoKnownAs', async () => {
await api('/i/known-as', {
alsoKnownAs: `@alice@${url.hostname}`,
}, bob);
await api('/i/known-as', {
alsoKnownAs: `@carol@${url.hostname}`,
await api('/i/update', {
alsoKnownAs: [`@alice@${url.hostname}`, `@carol@${url.hostname}`],
}, bob);
const newBob = await Users.findOneByOrFail({ id: bob.id });
@ -114,17 +108,31 @@ describe('Account Move', () => {
assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${carol.id}`);
});
test('Able to properly overwrite alsoKnownAs', async () => {
await api('/i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/i/update', {
alsoKnownAs: [`@carol@${url.hostname}`, `@dave@${url.hostname}`],
}, bob);
const newBob = await Users.findOneByOrFail({ id: bob.id });
assert.strictEqual(newBob.alsoKnownAs?.length, 2);
assert.strictEqual(newBob.alsoKnownAs[0], `${url.origin}/users/${carol.id}`);
assert.strictEqual(newBob.alsoKnownAs[1], `${url.origin}/users/${dave.id}`);
});
test('Unable to create an alias without the second @', async () => {
const res1 = await api('/i/known-as', {
alsoKnownAs: '@alice',
const res1 = await api('/i/update', {
alsoKnownAs: ['@alice'],
}, bob);
assert.strictEqual(res1.status, 400);
assert.strictEqual(res1.body.error.code, 'NO_SUCH_USER');
assert.strictEqual(res1.body.error.id, 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5');
const res2 = await api('/i/known-as', {
alsoKnownAs: 'alice',
const res2 = await api('/i/update', {
alsoKnownAs: ['alice'],
}, bob);
assert.strictEqual(res2.status, 400);
@ -137,8 +145,8 @@ describe('Account Move', () => {
let antennaId = '';
beforeAll(async () => {
await api('/i/known-as', {
alsoKnownAs: `@alice@${url.hostname}`,
await api('/i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, root);
const list = await api('/users/lists/create', {
name: rndstr('0-9a-z', 8),
@ -167,8 +175,8 @@ describe('Account Move', () => {
}, alice);
antennaId = antenna.body.id;
await api('/i/known-as', {
alsoKnownAs: `@alice@${url.hostname}`,
await api('/i/update', {
alsoKnownAs: [`@alice@${url.hostname}`],
}, bob);
await api('/following/create', {
@ -342,7 +350,7 @@ describe('Account Move', () => {
'/gallery/posts/like',
'/gallery/posts/unlike',
'/gallery/posts/update',
'/i/known-as',
'/i/update',
'/i/move',
'/notes/create',
'/notes/polls/vote',

View file

@ -2,35 +2,51 @@
<div class="_gaps_m">
<FormSection first>
<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
<MkInput v-model="moveToAccount" manual-save>
<template #prefix><i class="ti ti-plane-departure"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
</MkInput>
<div class="_gaps_m">
<div>
<MkInput v-model="moveToAccount">
<template #prefix><i class="ti ti-plane-departure"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
</MkInput>
</div>
<div>
<MkButton inline primary :disabled="!moveToAccount" @click="move"><i class="ti ti-check"></i> {{ i18n.ts.ok }}</MkButton>
</div>
</div>
</FormSection>
<FormInfo warn>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo>
<FormSection>
<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
<MkInput v-model="accountAlias" manual-save>
<template #prefix><i class="ti ti-plane-arrival"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveFromLabel }}</template>
</MkInput>
<div class="_gaps_m">
<div v-for="(_, i) in accountAliases">
<MkInput v-model="accountAliases[i]">
<template #prefix><i class="ti ti-plane-arrival"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template>
</MkInput>
</div>
<div>
<MkButton :disabled="accountAliases.length >= 5" inline style="margin-right: 8px;" @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</FormSection>
<FormInfo warn>{{ i18n.ts._accountMigration.moveFromDescription }}</FormInfo>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { ref } from 'vue';
import FormSection from '@/components/form/section.vue';
import FormInfo from '@/components/MkInfo.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const moveToAccount = ref('');
const accountAlias = ref('');
const accountAliases = ref(['']);
async function move(): Promise<void> {
const account = moveToAccount.value;
@ -44,20 +60,16 @@ async function move(): Promise<void> {
});
}
async function save(): Promise<void> {
const account = accountAlias.value;
os.apiWithDialog('i/known-as', {
alsoKnownAs: account,
});
function add() {
accountAliases.value.push('');
}
watch(accountAlias, async () => {
await save();
});
watch(moveToAccount, async () => {
await move();
});
async function save(): Promise<void> {
const alsoKnownAs = accountAliases.value.map(alias => alias.trim()).filter(alias => alias !== '');
os.apiWithDialog('i/update', {
alsoKnownAs,
});
}
definePageMetadata({
title: i18n.ts.accountMigration,

View file

@ -1357,10 +1357,6 @@ export type Endpoints = {
req: TODO;
res: TODO;
};
'i/known-as': {
req: TODO;
res: TODO;
};
'i/notifications': {
req: {
limit?: number;
@ -1511,6 +1507,7 @@ export type Endpoints = {
mutedWords?: string[][];
mutingNotificationTypes?: Notification_2['type'][];
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
};
res: MeDetailed;
};
@ -2348,6 +2345,7 @@ type LiteInstanceMetadata = {
imageUrl: string;
}[];
translatorAvailable: boolean;
serverRules: string[];
};
// @public (undocumented)
@ -2663,6 +2661,7 @@ type UserDetailed = UserLite & {
lang: string | null;
lastFetchedAt?: DateString;
location: string | null;
movedToUri: string;
notesCount: number;
pinnedNoteIds: ID[];
pinnedNotes: Note[];
@ -2697,7 +2696,6 @@ type UserLite = {
avatarUrl: string;
avatarBlurhash: string;
alsoKnownAs: string[];
movedToUri: any;
emojis: {
name: string;
url: string;

View file

@ -363,7 +363,6 @@ export type Endpoints = {
'i/import-following': { req: TODO; res: TODO; };
'i/import-user-lists': { req: TODO; res: TODO; };
'i/move': { req: TODO; res: TODO; };
'i/known-as': { req: TODO; res: TODO; };
'i/notifications': { req: {
limit?: number;
sinceId?: Notification['id'];
@ -421,6 +420,7 @@ export type Endpoints = {
mutedWords?: string[][];
mutingNotificationTypes?: Notification['type'][];
emailNotificationTypes?: string[];
alsoKnownAs?: string[];
}; res: MeDetailed; };
'i/user-group-invites': { req: TODO; res: TODO; };
'i/2fa/done': { req: TODO; res: TODO; };