merge: Fix account migrations (!1229)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1229 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
30d41ba671
36 changed files with 655 additions and 97 deletions
|
|
@ -9,6 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { isLocalUser } from '@/models/User.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository, NoteScheduleRepository, MiNoteSchedule } from '@/models/_.js';
|
||||
import type { RelationshipJobData, ThinUser } from '@/queue/types.js';
|
||||
|
||||
|
|
@ -29,13 +30,25 @@ import { AntennaService } from '@/core/AntennaService.js';
|
|||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import { TimeService } from '@/global/TimeService.js';
|
||||
import { InternalEventService } from '@/global/InternalEventService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMoveService {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.config)
|
||||
private readonly config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
|
|
@ -74,7 +87,27 @@ export class AccountMoveService {
|
|||
private readonly cacheService: CacheService,
|
||||
private readonly userListService: UserListService,
|
||||
private readonly timeService: TimeService,
|
||||
private readonly internalEventService: InternalEventService,
|
||||
private readonly loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('account-move');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async restartMigration(src: MiUser): Promise<void> {
|
||||
if (!src.movedToUri) {
|
||||
throw new IdentifiableError('ddcf173a-00f2-4aa4-ba12-cddd131bacf4', `Can't restart migrated for user ${src.id}: user has not migrated`);
|
||||
}
|
||||
|
||||
const dst = await this.apPersonService.resolvePerson(src.movedToUri);
|
||||
this.logger.info(`Restarting migration from ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host})`);
|
||||
|
||||
if (isLocalUser(src)) {
|
||||
// This calls createMoveJob at the end
|
||||
await this.moveFromLocal(src, dst);
|
||||
} else {
|
||||
await this.queueService.createMoveJob(src, dst);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -83,7 +116,7 @@ export class AccountMoveService {
|
|||
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
|
||||
*/
|
||||
@bindThis
|
||||
public async moveFromLocal(src: MiLocalUser, dst: MiLocalUser | MiRemoteUser): Promise<unknown> {
|
||||
public async moveFromLocal(src: MiLocalUser, dst: MiLocalUser | MiRemoteUser): Promise<Packed<'MeDetailed'>> {
|
||||
const srcUri = this.userEntityService.getUserUri(src);
|
||||
const dstUri = this.userEntityService.getUserUri(dst);
|
||||
|
||||
|
|
@ -96,7 +129,7 @@ export class AccountMoveService {
|
|||
Object.assign(src, update);
|
||||
|
||||
// Update cache
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', src);
|
||||
await this.internalEventService.emit('localUserUpdated', { id: src.id });
|
||||
|
||||
const srcPerson = await this.apRendererService.renderPerson(src);
|
||||
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
||||
|
|
@ -114,12 +147,12 @@ export class AccountMoveService {
|
|||
|
||||
// Unfollow after 24 hours
|
||||
const followings = await this.cacheService.userFollowingsCache.fetch(src.id);
|
||||
this.queueService.createDelayedUnfollowJob(Array.from(followings.keys()).map(followeeId => ({
|
||||
await this.queueService.createDelayedUnfollowJob(Array.from(followings.keys()).map(followeeId => ({
|
||||
from: { id: src.id },
|
||||
to: { id: followeeId },
|
||||
})), process.env.NODE_ENV === 'test' ? 10000 : 1000 * 60 * 60 * 24);
|
||||
|
||||
await this.postMoveProcess(src, dst);
|
||||
await this.queueService.createMoveJob(src, dst);
|
||||
|
||||
return iObj;
|
||||
}
|
||||
|
|
@ -127,18 +160,18 @@ export class AccountMoveService {
|
|||
@bindThis
|
||||
public async postMoveProcess(src: MiUser, dst: MiUser): Promise<void> {
|
||||
// Copy blockings and mutings, and update lists
|
||||
try {
|
||||
await Promise.all([
|
||||
this.copyBlocking(src, dst),
|
||||
this.copyMutings(src, dst),
|
||||
this.deleteScheduledNotes(src),
|
||||
this.copyRoles(src, dst),
|
||||
this.updateLists(src, dst),
|
||||
this.antennaService.onMoveAccount(src, dst),
|
||||
]);
|
||||
} catch {
|
||||
/* skip if any error happens */
|
||||
}
|
||||
await this.copyBlocking(src, dst)
|
||||
.catch(err => this.logger.warn(`Error copying blockings in migration ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`));
|
||||
await this.copyMutings(src, dst)
|
||||
.catch(err => this.logger.warn(`Error copying mutings in migration ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`));
|
||||
await this.deleteScheduledNotes(src)
|
||||
.catch(err => this.logger.warn(`Error deleting scheduled notes in migration ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`));
|
||||
await this.copyRoles(src, dst)
|
||||
.catch(err => this.logger.warn(`Error copying roles in migration ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`));
|
||||
await this.updateLists(src, dst)
|
||||
.catch(err => this.logger.warn(`Error updating lists in migration ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`));
|
||||
await this.antennaService.onMoveAccount(src, dst)
|
||||
.catch(err => this.logger.warn(`Error updating antennas in migration ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`));
|
||||
|
||||
// follow the new account
|
||||
const proxy = await this.systemAccountService.fetch('proxy');
|
||||
|
|
@ -153,29 +186,33 @@ export class AccountMoveService {
|
|||
// Decrease following count instead of unfollowing.
|
||||
try {
|
||||
await this.adjustFollowingCounts(followJobs.map(job => job.from.id), src);
|
||||
} catch {
|
||||
} catch (err) {
|
||||
/* skip if any error happens */
|
||||
this.logger.warn(`Non-fatal exception in migration from ${src.id} (@${src.usernameLower}@${src.host ?? this.config.host}) to ${dst.id} (@${dst.usernameLower}@${dst.host ?? this.config.host}): ${renderInlineError(err)}`);
|
||||
}
|
||||
|
||||
// Should be queued because this can cause a number of follow per one move.
|
||||
this.queueService.createFollowJob(followJobs);
|
||||
await this.queueService.createFollowJob(followJobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async copyBlocking(src: ThinUser, dst: ThinUser): Promise<void> {
|
||||
// Followers shouldn't overlap with blockers, but the destination account, different from the blockee (i.e., old account), may have followed the local user before moving.
|
||||
// So block the destination account here.
|
||||
const srcBlockings = await this.blockingsRepository.findBy({ blockeeId: src.id });
|
||||
const dstBlockings = await this.blockingsRepository.findBy({ blockeeId: dst.id });
|
||||
const blockerIds = dstBlockings.map(blocking => blocking.blockerId);
|
||||
const [srcBlockers, dstBlockers, dstFollowers] = await Promise.all([
|
||||
this.cacheService.userBlockedCache.fetch(src.id),
|
||||
this.cacheService.userBlockedCache.fetch(dst.id),
|
||||
this.cacheService.userFollowersCache.fetch(dst.id),
|
||||
]);
|
||||
// reblock the destination account
|
||||
const blockJobs: RelationshipJobData[] = [];
|
||||
for (const blocking of srcBlockings) {
|
||||
if (blockerIds.includes(blocking.blockerId)) continue; // skip if already blocked
|
||||
blockJobs.push({ from: { id: blocking.blockerId }, to: { id: dst.id } });
|
||||
for (const blockerId of srcBlockers) {
|
||||
if (dstBlockers.has(blockerId)) continue; // skip if already blocked
|
||||
if (dstFollowers.has(blockerId)) continue; // skip if already following
|
||||
blockJobs.push({ from: { id: blockerId }, to: { id: dst.id } });
|
||||
}
|
||||
// no need to unblock the old account because it may be still functional
|
||||
this.queueService.createBlockJob(blockJobs);
|
||||
await this.queueService.createBlockJob(blockJobs);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -188,9 +225,12 @@ export class AccountMoveService {
|
|||
if (oldMutings.length === 0) return;
|
||||
|
||||
// Check if the destination account is already indefinitely muted by the muter
|
||||
const existingMutingsMuterUserIds = await this.mutingsRepository.findBy(
|
||||
{ muteeId: dst.id, expiresAt: IsNull() },
|
||||
).then(mutings => mutings.map(muting => muting.muterId));
|
||||
const [existingMutingsMuterUserIds, dstFollowers] = await Promise.all([
|
||||
this.mutingsRepository.findBy(
|
||||
{ muteeId: dst.id, expiresAt: IsNull() },
|
||||
).then(mutings => mutings.map(muting => muting.muterId)),
|
||||
this.cacheService.userFollowersCache.fetch(dst.id),
|
||||
]);
|
||||
|
||||
const newMutings: Map<string, { muterId: string; muteeId: string; expiresAt: Date | null; }> = new Map();
|
||||
|
||||
|
|
@ -204,6 +244,7 @@ export class AccountMoveService {
|
|||
};
|
||||
for (const muting of oldMutings) {
|
||||
if (existingMutingsMuterUserIds.includes(muting.muterId)) continue; // skip if already muted indefinitely
|
||||
if (dstFollowers.has(muting.muterId)) continue; // skip if already following
|
||||
newMutings.set(genId(), {
|
||||
...muting,
|
||||
muteeId: dst.id,
|
||||
|
|
@ -304,22 +345,30 @@ export class AccountMoveService {
|
|||
if (localFollowerIds.length === 0) return;
|
||||
|
||||
// Set the old account's following and followers counts to 0.
|
||||
// TODO use CollapsedQueueService when merged
|
||||
await this.usersRepository.update({ id: oldAccount.id }, { followersCount: 0, followingCount: 0 });
|
||||
await this.internalEventService.emit(oldAccount.host == null ? 'localUserUpdated' : 'remoteUserUpdated', { id: oldAccount.id });
|
||||
|
||||
// Decrease following counts of local followers by 1.
|
||||
// TODO use CollapsedQueueService when merged
|
||||
await this.usersRepository.decrement({ id: In(localFollowerIds) }, 'followingCount', 1);
|
||||
await this.internalEventService.emit('usersUpdated', { ids: localFollowerIds });
|
||||
|
||||
// Decrease follower counts of local followees by 1.
|
||||
const oldFollowings = await this.cacheService.userFollowingsCache.fetch(oldAccount.id);
|
||||
if (oldFollowings.size > 0) {
|
||||
await this.usersRepository.decrement({ id: In(Array.from(oldFollowings.keys())) }, 'followersCount', 1);
|
||||
const oldFolloweeIds = Array.from(oldFollowings.keys());
|
||||
if (oldFolloweeIds.length > 0) {
|
||||
// TODO use CollapsedQueueService when merged
|
||||
await this.usersRepository.decrement({ id: In(oldFolloweeIds) }, 'followersCount', 1);
|
||||
await this.internalEventService.emit('usersUpdated', { ids: oldFolloweeIds });
|
||||
}
|
||||
|
||||
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||
this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
// TODO use CollapsedQueueService when merged
|
||||
await this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
|
||||
await this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
});
|
||||
|
||||
this.userMutingsCache = this.cacheManagementService.createQuantumKVCache<Set<string>>('userMutings', {
|
||||
lifetime: 1000 * 60 * 30, // 3m (workaround for mute expiration)
|
||||
lifetime: 1000 * 60 * 3, // 3m (workaround for mute expiration)
|
||||
fetcher: async muterId => {
|
||||
const mutings = await this.mutingsRepository.find({ where: { muterId: muterId }, select: ['muteeId'] });
|
||||
return new Set(mutings.map(muting => muting.muteeId));
|
||||
|
|
@ -283,7 +283,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
});
|
||||
|
||||
this.userMutedCache = this.cacheManagementService.createQuantumKVCache<Set<string>>('userMuted', {
|
||||
lifetime: 1000 * 60 * 30, // 3m (workaround for mute expiration)
|
||||
lifetime: 1000 * 60 * 3, // 3m (workaround for mute expiration)
|
||||
fetcher: async muteeId => {
|
||||
const mutings = await this.mutingsRepository.find({ where: { muteeId }, select: ['muterId'] });
|
||||
return new Set(mutings.map(muting => muting.muterId));
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { LatestNoteService } from '@/core/LatestNoteService.js';
|
|||
import { ApLogService } from '@/core/ApLogService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { TimeService } from '@/global/TimeService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -108,7 +109,7 @@ export class NoteDeleteService {
|
|||
? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user)
|
||||
: this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user));
|
||||
|
||||
this.deliverToConcerned(user, note, content);
|
||||
trackPromise(this.deliverToConcerned(user, note, content));
|
||||
}
|
||||
|
||||
// also deliver delete activity to cascaded notes
|
||||
|
|
@ -117,7 +118,7 @@ export class NoteDeleteService {
|
|||
if (!cascadingNote.user) continue;
|
||||
if (!isLocalUser(cascadingNote.user)) continue;
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
|
||||
this.deliverToConcerned(cascadingNote.user, cascadingNote, content);
|
||||
trackPromise(this.deliverToConcerned(cascadingNote.user, cascadingNote, content));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export class NotePiningService {
|
|||
|
||||
// Deliver to remote followers
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
this.deliverPinnedChange(user, note.id, true);
|
||||
await this.deliverPinnedChange(user, note.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,14 +105,14 @@ export class NotePiningService {
|
|||
throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', `Note ${noteId} does not exist`);
|
||||
}
|
||||
|
||||
this.userNotePiningsRepository.delete({
|
||||
await this.userNotePiningsRepository.delete({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
// Deliver to remote followers
|
||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
|
||||
this.deliverPinnedChange(user, noteId, false);
|
||||
await this.deliverPinnedChange(user, noteId, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -763,7 +763,13 @@ export class QueueService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock', data: RelationshipJobData, opts: Bull.JobsOptions = {}): {
|
||||
public createMoveJob(from: ThinUser, to: ThinUser) {
|
||||
const job = this.generateRelationshipJobData('move', { from, to });
|
||||
return this.relationshipQueue.add(job.name, job.data, job.opts);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateRelationshipJobData(name: 'follow' | 'unfollow' | 'block' | 'unblock' | 'move', data: RelationshipJobData, opts: Bull.JobsOptions = {}): {
|
||||
name: string,
|
||||
data: RelationshipJobData,
|
||||
opts: Bull.JobsOptions,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,15 @@ import { deepClone } from '@/misc/clone.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { CacheManagementService, ManagedMemorySingleCache } from '@/global/CacheManagementService.js';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||
import type { Signed } from '@/core/activitypub/JsonLdService.js';
|
||||
|
||||
@Injectable()
|
||||
export class RelayService {
|
||||
private readonly logger: Logger;
|
||||
private readonly relaysCache: ManagedMemorySingleCache<MiRelay[]>;
|
||||
|
||||
constructor(
|
||||
|
|
@ -29,9 +35,11 @@ export class RelayService {
|
|||
private queueService: QueueService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private apRendererService: ApRendererService,
|
||||
private readonly loggerService: LoggerService,
|
||||
|
||||
cacheManagementService: CacheManagementService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('relay');
|
||||
this.relaysCache = cacheManagementService.createMemorySingleCache<MiRelay[]>('relay', 1000 * 60 * 10); // 10m
|
||||
}
|
||||
|
||||
|
|
@ -106,10 +114,19 @@ export class RelayService {
|
|||
const copy = deepClone(activity);
|
||||
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
|
||||
const signed = await this.apRendererService.attachLdSignature(copy, user);
|
||||
const signed = await this.signActivity(copy, user);
|
||||
|
||||
for (const relay of relays) {
|
||||
this.queueService.deliver(user, signed, relay.inbox, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async signActivity<T extends IActivity>(activity: T, user: { id: MiUser['id']; host: null; }): Promise<T | Signed<T>> {
|
||||
try {
|
||||
return await this.apRendererService.attachLdSignature(activity, user);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Error signing activity ${activity.id}: ${renderInlineError(err)}`);
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ export class WebhookTestService {
|
|||
url: null,
|
||||
uri: null,
|
||||
movedTo: null,
|
||||
alsoKnownAs: [],
|
||||
movedToUri: null,
|
||||
createdAt: this.timeService.date.toISOString(),
|
||||
updatedAt: user.updatedAt?.toISOString() ?? null,
|
||||
lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { TimeService } from '@/global/TimeService.js';
|
||||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { JsonLdService, type Signed } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import { CONTEXT } from './misc/contexts.js';
|
||||
import { getApId, ILink, IOrderedCollection, IOrderedCollectionPage } from './type.js';
|
||||
|
|
@ -804,7 +804,7 @@ export class ApRendererService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
|
||||
public async attachLdSignature<T extends IActivity>(activity: T, user: { id: MiUser['id']; host: null; }): Promise<T | Signed<T>> {
|
||||
// Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
|
||||
// When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
|
||||
// This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
|
||||
|
|
|
|||
|
|
@ -523,6 +523,10 @@ const activitystreams = {
|
|||
'@id': 'as:alsoKnownAs',
|
||||
'@type': '@id',
|
||||
},
|
||||
'movedTo': {
|
||||
'@id': 'as:movedTo',
|
||||
'@type': '@id',
|
||||
},
|
||||
},
|
||||
} satisfies JsonLd;
|
||||
|
||||
|
|
@ -584,6 +588,7 @@ const extension_context_definition = {
|
|||
backgroundUrl: 'sharkey:backgroundUrl',
|
||||
listenbrainz: 'sharkey:listenbrainz',
|
||||
enableRss: 'sharkey:enableRss',
|
||||
noindex: 'sharkey:noindex',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
} satisfies Context;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { renderInlineError } from '@/misc/render-inline-error.js';
|
|||
import { extractMediaFromHtml } from '@/core/activitypub/misc/extract-media-from-html.js';
|
||||
import { extractMediaFromMfm } from '@/core/activitypub/misc/extract-media-from-mfm.js';
|
||||
import { getContentByType } from '@/core/activitypub/misc/get-content-by-type.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { CustomEmojiService, encodeEmojiKey, isValidEmojiName } from '@/core/CustomEmojiService.js';
|
||||
import { TimeService } from '@/global/TimeService.js';
|
||||
import { getOneApId, getApId, validPost, isEmoji, getApType, isApObject, isDocument, IApDocument, isLink } from '../type.js';
|
||||
|
|
@ -300,7 +301,7 @@ export class ApNoteService implements OnModuleInit {
|
|||
await this.pollService.vote(actor, reply, index);
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
this.pollService.deliverQuestionUpdate(reply);
|
||||
trackPromise(this.pollService.deliverQuestionUpdate(reply));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -479,7 +480,7 @@ export class ApNoteService implements OnModuleInit {
|
|||
await this.pollService.vote(actor, reply, index);
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
this.pollService.deliverQuestionUpdate(reply);
|
||||
trackPromise(this.pollService.deliverQuestionUpdate(reply));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import { verifyFieldLinks } from '@/misc/verify-field-link.js';
|
|||
import { isRetryableError } from '@/misc/is-retryable-error.js';
|
||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { getApId, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
|
|
@ -121,6 +122,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
private readonly apUtilityService: ApUtilityService,
|
||||
private readonly idService: IdService,
|
||||
private readonly timeService: TimeService,
|
||||
private readonly queueService: QueueService,
|
||||
|
||||
apLoggerService: ApLoggerService,
|
||||
) {
|
||||
|
|
@ -988,7 +990,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
return 'skip: alsoKnownAs does not include from.uri';
|
||||
}
|
||||
|
||||
await this.accountMoveService.postMoveProcess(src, dst);
|
||||
await this.queueService.createMoveJob(src, dst);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -409,6 +409,30 @@ export class UserEntityService implements OnModuleInit {
|
|||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async resolveAlsoKnownAs(user: MiUser): Promise<{ uri: string, id: string | null }[] | null> {
|
||||
if (!user.alsoKnownAs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const alsoKnownAs: { uri: string, id: string | null }[] = [];
|
||||
for (const uri of new Set(user.alsoKnownAs)) {
|
||||
try {
|
||||
const resolved = await this.apPersonService.resolvePerson(uri);
|
||||
alsoKnownAs.push({ uri, id: resolved.id });
|
||||
} catch {
|
||||
// ignore errors - we expect some users to be deleted or unavailable
|
||||
alsoKnownAs.push({ uri, id: null });
|
||||
}
|
||||
}
|
||||
|
||||
if (alsoKnownAs.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return alsoKnownAs;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getIdenticonUrl(user: MiUser): string {
|
||||
if ((user.host == null || user.host === this.config.host) && user.username.includes('.') && this.meta.iconUrl) { // ローカルのシステムアカウントの場合
|
||||
|
|
@ -551,6 +575,10 @@ export class UserEntityService implements OnModuleInit {
|
|||
let fetchPoliciesPromise: Promise<RolePolicies> | null = null;
|
||||
const fetchPolicies = () => fetchPoliciesPromise ??= this.roleService.getUserPolicies(user);
|
||||
|
||||
// This has a cache so it's fine to await here
|
||||
const alsoKnownAs = await this.resolveAlsoKnownAs(user);
|
||||
const alsoKnownAsIds = alsoKnownAs?.map(aka => aka.id).filter(id => id != null) ?? null;
|
||||
|
||||
const bypassSilence = isMe || (myFollowings ? myFollowings.has(user.id) : false);
|
||||
|
||||
const packed = {
|
||||
|
|
@ -617,10 +645,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
url: profile!.url,
|
||||
uri: user.uri,
|
||||
movedTo: user.movedToUri ? Promise.resolve(opts.userIdsByUri?.get(user.movedToUri) ?? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null)) : null,
|
||||
alsoKnownAs: user.alsoKnownAs
|
||||
? Promise.all(user.alsoKnownAs.map(uri => Promise.resolve(opts.userIdsByUri?.get(uri) ?? this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))))
|
||||
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null))
|
||||
: null,
|
||||
movedToUri: user.movedToUri,
|
||||
// alsoKnownAs moved from packedUserDetailedNotMeOnly for privacy
|
||||
bannerUrl: user.bannerId == null ? null : user.bannerUrl,
|
||||
bannerBlurhash: user.bannerId == null ? null : user.bannerBlurhash,
|
||||
backgroundUrl: user.backgroundId == null ? null : user.backgroundUrl,
|
||||
|
|
@ -711,6 +737,9 @@ export class UserEntityService implements OnModuleInit {
|
|||
defaultCW: profile!.defaultCW,
|
||||
defaultCWPriority: profile!.defaultCWPriority,
|
||||
allowUnsignedFetch: user.allowUnsignedFetch,
|
||||
// alsoKnownAs moved from packedUserDetailedNotMeOnly for privacy
|
||||
alsoKnownAs: alsoKnownAsIds,
|
||||
skAlsoKnownAs: alsoKnownAs,
|
||||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
|
|
|
|||
|
|
@ -309,19 +309,16 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
nullable: true, optional: false,
|
||||
},
|
||||
movedTo: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
movedToUri: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
alsoKnownAs: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
// alsoKnownAs moved to packedUserDetailedNotMeOnly for privacy
|
||||
bannerUrl: {
|
||||
type: 'string',
|
||||
format: 'url',
|
||||
|
|
@ -814,6 +811,36 @@ export const packedMeDetailedOnlySchema = {
|
|||
enum: userUnsignedFetchOptions,
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
// alsoKnownAs moved from packedUserDetailedNotMeOnly for privacy
|
||||
alsoKnownAs: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
skAlsoKnownAs: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'id',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -429,6 +429,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
|
||||
case 'block': return this.relationshipProcessorService.processBlock(job);
|
||||
case 'unblock': return this.relationshipProcessorService.processUnblock(job);
|
||||
case 'move': return this.relationshipProcessorService.processMove(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
||||
|
|
@ -28,8 +30,10 @@ export class RelationshipProcessorService {
|
|||
private queueLoggerService: QueueLoggerService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly accountMoveService: AccountMoveService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('follow-block');
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('relationship');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -47,8 +51,8 @@ export class RelationshipProcessorService {
|
|||
public async processUnfollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to unfollow ${job.data.to.id}`);
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||
this.cacheService.findUserById(job.data.from.id),
|
||||
this.cacheService.findUserById(job.data.to.id),
|
||||
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
|
||||
await this.userFollowingService.unfollow(follower, followee, job.data.silent);
|
||||
return 'ok';
|
||||
|
|
@ -58,8 +62,8 @@ export class RelationshipProcessorService {
|
|||
public async processBlock(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to block ${job.data.to.id}`);
|
||||
const [blockee, blocker] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||
this.cacheService.findUserById(job.data.from.id),
|
||||
this.cacheService.findUserById(job.data.to.id),
|
||||
]);
|
||||
await this.userBlockingService.block(blockee, blocker, job.data.silent);
|
||||
return 'ok';
|
||||
|
|
@ -69,10 +73,20 @@ export class RelationshipProcessorService {
|
|||
public async processUnblock(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to unblock ${job.data.to.id}`);
|
||||
const [blockee, blocker] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
|
||||
this.cacheService.findUserById(job.data.from.id),
|
||||
this.cacheService.findUserById(job.data.to.id),
|
||||
]);
|
||||
await this.userBlockingService.unblock(blockee, blocker);
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
public async processMove(job: Bull.Job<RelationshipJobData>): Promise<string> {
|
||||
this.logger.info(`${job.data.from.id} is trying to migrate to ${job.data.to.id}`);
|
||||
const [src, dst] = await Promise.all([
|
||||
this.cacheService.findUserById(job.data.from.id),
|
||||
this.cacheService.findUserById(job.data.to.id),
|
||||
]);
|
||||
await this.accountMoveService.postMoveProcess(src, dst);
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
|
|||
export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
|
||||
export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
|
||||
export * as 'admin/reset-password' from './endpoints/admin/reset-password.js';
|
||||
export * as 'admin/restart-migration' from './endpoints/admin/restart-migration.js';
|
||||
export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js';
|
||||
export * as 'admin/roles/annotate-condition' from './endpoints/admin/roles/annotate-condition.js';
|
||||
export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
kind: 'write:admin:restart-migration',
|
||||
|
||||
errors: {
|
||||
accountHasNotMigrated: {
|
||||
message: 'Account has not migrated anywhere.',
|
||||
code: 'ACCOUNT_HAS_NOT_MIGRATED',
|
||||
id: 'ddcf173a-00f2-4aa4-ba12-cddd131bacf4',
|
||||
},
|
||||
},
|
||||
|
||||
res: {},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly moderationLogService: ModerationLogService,
|
||||
private readonly accountMoveService: AccountMoveService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
await this.accountMoveService.restartMigration(user);
|
||||
|
||||
await this.moderationLogService.log(me, 'restartMigration', {
|
||||
userId: user.id,
|
||||
userUsername: user.username,
|
||||
userHost: user.host,
|
||||
});
|
||||
} catch (err) {
|
||||
// TODO allow this mapping stuff to be defined in the meta
|
||||
if (err instanceof IdentifiableError && err.id === 'ddcf173a-00f2-4aa4-ba12-cddd131bacf4') {
|
||||
throw new ApiError(meta.errors.accountHasNotMigrated);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,8 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||
import { isSystemAccount } from '@/misc/is-system-account.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
|
@ -225,6 +227,46 @@ export const meta = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
movedAt: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
movedTo: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserDetailed',
|
||||
nullable: true, optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
alsoKnownAs: {
|
||||
type: 'array',
|
||||
nullable: true, optional: true,
|
||||
items: {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserDetailed',
|
||||
nullable: true, optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -253,6 +295,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private roleEntityService: RoleEntityService,
|
||||
private idService: IdService,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly apPersonService: ApPersonService,
|
||||
private readonly userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const [user, profile] = await Promise.all([
|
||||
|
|
@ -280,6 +324,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const followStats = await this.cacheService.getFollowStats(user.id);
|
||||
|
||||
const movedAt = user.movedAt?.toISOString();
|
||||
|
||||
const movedToUser = user.movedToUri ? await this.apPersonService.resolvePerson(user.movedToUri) : null;
|
||||
const movedTo = user.movedToUri ? {
|
||||
uri: user.movedToUri,
|
||||
user: movedToUser ? await this.userEntityService.pack(movedToUser, me, { schema: 'UserDetailed' }) : undefined,
|
||||
} : null;
|
||||
|
||||
// This is kinda heavy, but it's an admin endpoint so ok.
|
||||
const aka = await this.userEntityService.resolveAlsoKnownAs(user);
|
||||
const akaUsers = aka ? await this.userEntityService.packMany(aka.map(aka => aka.id).filter(id => id != null), me, { schema: 'UserDetailed' }) : [];
|
||||
const alsoKnownAs = aka?.map(aka => ({
|
||||
uri: aka.uri,
|
||||
user: aka.id ? akaUsers.find(u => u.id === aka.id) : undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
|
|
@ -318,6 +378,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
totalFollowers: Math.max(user.followersCount, followStats.localFollowers + followStats.remoteFollowers),
|
||||
totalFollowing: Math.max(user.followingCount, followStats.localFollowing + followStats.remoteFollowing),
|
||||
},
|
||||
movedAt,
|
||||
movedTo,
|
||||
alsoKnownAs,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
|||
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
|
||||
import { userUnsignedFetchOptions } from '@/const.js';
|
||||
import { renderInlineError } from '@/misc/render-inline-error.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { ApiLoggerService } from '../../ApiLoggerService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
|
|
@ -640,12 +641,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
|
||||
if (user.isLocked && ps.isLocked === false) {
|
||||
this.userFollowingService.acceptAllFollowRequests(user);
|
||||
await this.userFollowingService.acceptAllFollowRequests(user);
|
||||
}
|
||||
|
||||
// フォロワーにUpdateを配信
|
||||
if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) {
|
||||
this.accountUpdateService.publishToFollowers(user);
|
||||
trackPromise(this.accountUpdateService.publishToFollowers(user));
|
||||
}
|
||||
|
||||
return iObj;
|
||||
|
|
|
|||
|
|
@ -172,11 +172,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (note.userHost != null) {
|
||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as MiRemoteUser;
|
||||
|
||||
this.queueService.deliver(me, this.apRendererService.addContext(await this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false);
|
||||
this.queueService.deliver(me, this.apRendererService.addContext(this.apRendererService.renderVote(me, vote, note, poll, pollOwner)), pollOwner.inbox, false);
|
||||
}
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
this.pollService.deliverQuestionUpdate(note);
|
||||
await this.pollService.deliverQuestionUpdate(note);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export const moderationLogTypes = [
|
|||
'deleteGlobalAnnouncement',
|
||||
'deleteUserAnnouncement',
|
||||
'resetPassword',
|
||||
'restartMigration',
|
||||
'setMandatoryCW',
|
||||
'setMandatoryCWForNote',
|
||||
'setMandatoryCWForInstance',
|
||||
|
|
@ -290,6 +291,11 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
restartMigration: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
setMandatoryCW: {
|
||||
newCW: string | null;
|
||||
oldCW: string | null;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export const Default = {
|
|||
},
|
||||
args: {
|
||||
movedTo: userDetailed().id,
|
||||
movedToUri: 'https://example.com',
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
|
|
|||
|
|
@ -7,25 +7,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="user" :class="$style.root">
|
||||
<i class="ti ti-plane-departure" style="margin-right: 8px;"></i>
|
||||
{{ i18n.ts.accountMoved }}
|
||||
<MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
|
||||
<MkMention v-if="user" :class="$style.link" :username="user.username" :host="user.host ?? localHost"/>
|
||||
<MkLink v-else :url="movedToUri"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkMention from './MkMention.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { host as localHost } from '@@/js/config.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
const user = ref<Misskey.entities.UserLite>();
|
||||
const user = ref<Misskey.entities.UserLite | null>();
|
||||
|
||||
const props = defineProps<{
|
||||
movedTo: string; // user id
|
||||
movedTo?: string | null; // user id
|
||||
movedToUri: string; // user URI
|
||||
}>();
|
||||
|
||||
misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
||||
watch(() => props.movedTo, () => {
|
||||
user.value = null;
|
||||
if (props.movedTo) {
|
||||
misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :spacer="true" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
|
||||
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs" :spacer="true" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px; --MI_SPACER-h: var(--MI-marginHalf)">
|
||||
<FormSuspense v-if="init" :p="init">
|
||||
<div v-if="user && info">
|
||||
<div v-if="tab === 'overview'" class="_gaps">
|
||||
<MkAccountMoved v-if="user.movedToUri" :movedTo="user.movedTo" :movedToUri="user.movedToUri"/>
|
||||
|
||||
<div class="aeakzknw">
|
||||
<MkAvatar class="avatar" :user="user" indicator link preview/>
|
||||
<div class="body">
|
||||
|
|
@ -104,6 +106,45 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="info.movedTo || info.alsoKnownAs" :sticky="false">
|
||||
<template #icon><i class="ph-airplane ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.accountMigration }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<FormSection v-if="info.movedTo" first>
|
||||
<template #label>{{ i18n.ts.newAccount }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.accountMigrationUri }}</template>
|
||||
<template #value><MkLink :url="info.movedTo.uri">{{ info.movedTo.uri }}</MkLink></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info.movedAt" oneline>
|
||||
<template #key>{{ i18n.ts.accountMigratedAt }}</template>
|
||||
<template #value><MkTime :time="info.movedAt" :mode="'detail'"/></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="info.movedTo.user" oneline>
|
||||
<template #key>{{ i18n.ts.accountMigratedTo }}</template>
|
||||
<template #value><MkMention :username="info.movedTo.user.username" :host="info.movedTo.user.host ?? localHost"/></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSection v-if="info.alsoKnownAs?.length" first>
|
||||
<template #label>{{ i18n.ts.alsoKnownAs }}</template>
|
||||
|
||||
<ul class="_gaps_s">
|
||||
<li v-for="aka of info.alsoKnownAs" :key="aka.uri">
|
||||
<div style="display: flex; flex-direction: row; gap: 0.75em; align-items: center">
|
||||
<MkMention v-if="aka.user" :username="aka.user.username" :host="aka.user.host ?? localHost"/>
|
||||
<MkLink :url="aka.uri">({{ aka.uri }})</MkLink>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</FormSection>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="iAmModerator" :defaultOpen="moderationNote.length > 0" :sticky="false">
|
||||
<template #icon><i class="ph-stamp ph-bold ph-lg"></i></template>
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
|
|
@ -116,17 +157,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormSection v-if="user?.host">
|
||||
<template #label>{{ i18n.ts.activityPub }}</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.instanceInfo }}</template>
|
||||
<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ti ti-chevron-right"></i></MkA></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.updatedAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="user.lastFetchedAt"/></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1em;">
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.instanceInfo }}</template>
|
||||
<template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="ti ti-chevron-right"></i></MkA></template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue oneline>
|
||||
<template #key>{{ i18n.ts.updatedAt }}</template>
|
||||
<template #value><MkTime mode="detail" :time="user.lastFetchedAt"/></template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
|
@ -150,6 +189,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.buttonStrip">
|
||||
<MkButton v-if="user.host != null" inline @click="updateRemoteUser"><i class="ph-cloud-arrow-down ph-bold ph-lg"></i> {{ i18n.ts.updateRemoteUser }}</MkButton>
|
||||
<MkButton v-if="user.host == null" inline accent @click="resetPassword"><i class="ph-password ph-bold ph-lg"></i> {{ i18n.ts.resetPassword }}</MkButton>
|
||||
<MkButton v-if="info.movedTo && iAmAdmin" inline @click="restartMigration"><i class="ph-airplane-takeoff ph-bold ph-lg"></i> {{ i18n.ts.restartMigration }}</MkButton>
|
||||
<MkButton inline accent @click="unsetUserAvatar"><i class="ph-camera-slash ph-bold ph-lg"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
||||
<MkButton inline accent @click="unsetUserBanner"><i class="ph-image-broken ph-bold ph-lg"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
||||
<MkButton inline danger @click="deleteAllFiles"><i class="ph-trash ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
|
||||
|
|
@ -279,7 +319,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { host as localHost, url } from '@@/js/config.js';
|
||||
import type { Badge } from '@/components/SkBadgeStrip.vue';
|
||||
import type { ChartSrc } from '@/components/MkChart.vue';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
|
|
@ -307,6 +347,9 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
||||
import SkBadgeStrip from '@/components/SkBadgeStrip.vue';
|
||||
import MkAccountMoved from '@/components/MkAccountMoved.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkMention from '@/components/MkMention.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
userId: string;
|
||||
|
|
@ -455,14 +498,14 @@ function createFetcher(withHint = true) {
|
|||
return () => Promise.all([
|
||||
(withHint && props.userHint) ? props.userHint : misskeyApi('users/show', {
|
||||
userId: props.userId,
|
||||
}),
|
||||
}).catch(() => null),
|
||||
(withHint && props.infoHint) ? props.infoHint : misskeyApi('admin/show-user', {
|
||||
userId: props.userId,
|
||||
}),
|
||||
}).catch(() => null),
|
||||
iAmAdmin
|
||||
? (withHint && props.ipsHint) ? props.ipsHint : misskeyApi('admin/get-user-ips', {
|
||||
userId: props.userId,
|
||||
})
|
||||
}).catch(() => null)
|
||||
: null,
|
||||
iAmAdmin
|
||||
? (withHint && props.apHint) ? props.apHint : misskeyApi('ap/get', {
|
||||
|
|
@ -730,6 +773,19 @@ function editAnnouncement(announcement) {
|
|||
});
|
||||
}
|
||||
|
||||
async function restartMigration() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.restartMigration,
|
||||
text: i18n.ts.restartMigrationConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
await os.promiseDialog(async () => {
|
||||
await misskeyApi('admin/restart-migration', { userId: props.userId });
|
||||
await refreshUser();
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.userId, () => {
|
||||
init.value = createFetcher();
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
'importCustomEmojis',
|
||||
'createPromo',
|
||||
'addRelay',
|
||||
'restartMigration',
|
||||
].includes(log.type),
|
||||
[$style.logYellow]: [
|
||||
'markSensitiveDriveFile',
|
||||
|
|
@ -130,6 +131,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="log.type === 'createPromo'">: @{{ log.info.noteUserUsername }}{{ log.info.noteUserHost ? '@' + log.info.noteUserHost : '' }}</span>
|
||||
<span v-else-if="log.type === 'addRelay'">: {{ log.info.inbox }}</span>
|
||||
<span v-else-if="log.type === 'removeRelay'">: {{ log.info.inbox }}</span>
|
||||
<span v-else-if="log.type === 'restartMigration'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
|
||||
</template>
|
||||
<template #icon>
|
||||
<i v-if="log.type === 'updateServerSettings'" class="ti ti-settings"></i>
|
||||
|
|
@ -174,6 +176,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="log.type === 'deleteFlash'" class="ti ti-trash"></i>
|
||||
<i v-else-if="log.type === 'deleteGalleryPost'" class="ti ti-trash"></i>
|
||||
<i v-else-if="log.type === 'deleteChatRoom'" class="ti ti-trash"></i>
|
||||
<i v-else-if="log.type === 'restartMigration'" class="ph-airplane-takeoff ph-bold ph-lg"></i>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<MkTime :time="log.createdAt"/>
|
||||
|
|
@ -223,6 +226,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template v-else-if="log.type === 'unsuspend'">
|
||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'restartMigration'">
|
||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||
</template>
|
||||
<template v-else-if="log.type === 'rejectQuotesUser'">
|
||||
<div>{{ i18n.ts.user }}: <MkA :to="`/admin/user/${log.info.userId}`" class="_link">@{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</MkA></div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo v-if="user.isSilenced" :warn="true">{{ i18n.ts.userSilenced }}</MkInfo>
|
||||
|
||||
<div class="profile _gaps">
|
||||
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
||||
<MkAccountMoved v-if="user.movedToUri" :movedTo="user.movedTo" :movedToUri="user.movedToUri"/>
|
||||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!"/>
|
||||
<MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ rt {
|
|||
max-width: min(var(--MI_SPACER-w, 100%), calc(100% - (var(--MI_SPACER-max, 24px) * 2)));
|
||||
|
||||
/* marginを使って余白を表現すると、margin特有の親突き抜け仕様などが厄介になってくるので上下はpaddingを使う */
|
||||
padding: var(--MI_SPACER-max, 24px) 0;
|
||||
padding: var(--MI_SPACER-h, var(--MI_SPACER-max, 24px)) 0;
|
||||
margin: 0 auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
|
|
|||
|
|
@ -337,6 +337,12 @@ type AdminResetPasswordResponse = operations['admin___reset-password']['response
|
|||
// @public (undocumented)
|
||||
type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminRestartMigrationRequest = operations['admin___restart-migration']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminRestartMigrationResponse = operations['admin___restart-migration']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminRolesAnnotateConditionRequest = operations['admin___roles___annotate-condition']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
@ -1621,6 +1627,8 @@ declare namespace entities {
|
|||
AdminResetPasswordRequest,
|
||||
AdminResetPasswordResponse,
|
||||
AdminResolveAbuseUserReportRequest,
|
||||
AdminRestartMigrationRequest,
|
||||
AdminRestartMigrationResponse,
|
||||
AdminRolesAnnotateConditionRequest,
|
||||
AdminRolesAnnotateConditionResponse,
|
||||
AdminRolesAssignRequest,
|
||||
|
|
@ -2989,6 +2997,9 @@ type ModerationLog = {
|
|||
} | {
|
||||
type: 'setMandatoryCWForInstance';
|
||||
info: ModerationLogPayloads['setMandatoryCWForInstance'];
|
||||
} | {
|
||||
type: 'restartMigration';
|
||||
info: ModerationLogPayloads['restartMigration'];
|
||||
} | {
|
||||
type: 'resetPassword';
|
||||
info: ModerationLogPayloads['resetPassword'];
|
||||
|
|
@ -3440,7 +3451,7 @@ type PartialRolePolicyOverride = Partial<{
|
|||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "read:admin:abuse-report:notification-recipient", "write:admin:abuse-report:notification-recipient", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:cw-user", "write:admin:cw-note", "write:admin:cw-instance", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:reject-quotes", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:chat", "read:chat"];
|
||||
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "write:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "read:admin:abuse-report:notification-recipient", "write:admin:abuse-report:notification-recipient", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:cw-user", "write:admin:cw-note", "write:admin:cw-instance", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:reject-quotes", "write:admin:restart-migration", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:chat", "read:chat"];
|
||||
|
||||
// @public (undocumented)
|
||||
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
|
||||
|
|
|
|||
|
|
@ -867,6 +867,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:restart-migration*
|
||||
*/
|
||||
request<E extends 'admin/restart-migration', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ import type {
|
|||
AdminResetPasswordRequest,
|
||||
AdminResetPasswordResponse,
|
||||
AdminResolveAbuseUserReportRequest,
|
||||
AdminRestartMigrationRequest,
|
||||
AdminRestartMigrationResponse,
|
||||
AdminRolesAnnotateConditionRequest,
|
||||
AdminRolesAnnotateConditionResponse,
|
||||
AdminRolesAssignRequest,
|
||||
|
|
@ -750,6 +752,7 @@ export type Endpoints = {
|
|||
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
|
||||
'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
|
||||
'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
|
||||
'admin/restart-migration': { req: AdminRestartMigrationRequest; res: AdminRestartMigrationResponse };
|
||||
'admin/roles/annotate-condition': { req: AdminRolesAnnotateConditionRequest; res: AdminRolesAnnotateConditionResponse };
|
||||
'admin/roles/assign': { req: AdminRolesAssignRequest; res: EmptyResponse };
|
||||
'admin/roles/clone': { req: AdminRolesCloneRequest; res: AdminRolesCloneResponse };
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
|
|||
export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
|
||||
export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
|
||||
export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
|
||||
export type AdminRestartMigrationRequest = operations['admin___restart-migration']['requestBody']['content']['application/json'];
|
||||
export type AdminRestartMigrationResponse = operations['admin___restart-migration']['responses']['200']['content']['application/json'];
|
||||
export type AdminRolesAnnotateConditionRequest = operations['admin___roles___annotate-condition']['requestBody']['content']['application/json'];
|
||||
export type AdminRolesAnnotateConditionResponse = operations['admin___roles___annotate-condition']['responses']['200']['content']['application/json'];
|
||||
export type AdminRolesAssignRequest = operations['admin___roles___assign']['requestBody']['content']['application/json'];
|
||||
|
|
|
|||
|
|
@ -1724,6 +1724,28 @@ export type paths = {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/admin/restart-migration': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* admin/restart-migration
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:admin:restart-migration*
|
||||
*/
|
||||
post: operations['admin___restart-migration'];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/admin/roles/annotate-condition': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -10130,9 +10152,10 @@ export type components = {
|
|||
url: string | null;
|
||||
/** Format: uri */
|
||||
uri: string | null;
|
||||
/** Format: uri */
|
||||
/** Format: id */
|
||||
movedTo: string | null;
|
||||
alsoKnownAs: string[] | null;
|
||||
/** Format: uri */
|
||||
movedToUri: string | null;
|
||||
/** Format: url */
|
||||
bannerUrl: string | null;
|
||||
bannerBlurhash: string | null;
|
||||
|
|
@ -10390,6 +10413,13 @@ export type components = {
|
|||
defaultCWPriority: 'default' | 'parent' | 'defaultParent' | 'parentDefault';
|
||||
/** @enum {string} */
|
||||
allowUnsignedFetch: 'never' | 'always' | 'essential' | 'staff';
|
||||
alsoKnownAs: string[] | null;
|
||||
skAlsoKnownAs: {
|
||||
/** Format: uri */
|
||||
uri: string;
|
||||
/** Format: id */
|
||||
id: string | null;
|
||||
}[] | null;
|
||||
};
|
||||
UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'];
|
||||
MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
|
||||
|
|
@ -17753,6 +17783,78 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
'admin___restart-migration': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': unknown;
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
'admin___roles___annotate-condition': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -19030,6 +19132,17 @@ export interface operations {
|
|||
remoteFollowers: number;
|
||||
};
|
||||
signupReason: string | null;
|
||||
movedAt?: string | null;
|
||||
movedTo?: {
|
||||
/** Format: uri */
|
||||
uri: string;
|
||||
user?: components['schemas']['UserDetailed'] | null;
|
||||
} | null;
|
||||
alsoKnownAs?: {
|
||||
/** Format: uri */
|
||||
uri: string;
|
||||
user?: components['schemas']['UserDetailed'] | null;
|
||||
}[] | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export const permissions = [
|
|||
'write:admin:unset-user-banner',
|
||||
'write:admin:unsuspend-user',
|
||||
'write:admin:reject-quotes',
|
||||
'write:admin:restart-migration',
|
||||
'write:admin:meta',
|
||||
'write:admin:user-note',
|
||||
'write:admin:roles',
|
||||
|
|
@ -339,6 +340,11 @@ export type ModerationLogPayloads = {
|
|||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
restartMigration: {
|
||||
userId: string;
|
||||
userUsername: string;
|
||||
userHost: string | null;
|
||||
};
|
||||
setMandatoryCW: {
|
||||
newCW: string | null;
|
||||
oldCW: string | null;
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ export type ModerationLog = {
|
|||
} | {
|
||||
type: 'setMandatoryCWForInstance';
|
||||
info: ModerationLogPayloads['setMandatoryCWForInstance'];
|
||||
} | {
|
||||
type: 'restartMigration';
|
||||
info: ModerationLogPayloads['restartMigration'];
|
||||
} | {
|
||||
type: 'resetPassword';
|
||||
info: ModerationLogPayloads['resetPassword'];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue