mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-19 20:35:34 +02:00
Merge branch 'develop' into lowpowermode
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RemoteNotesCleaning1753863104203 {
|
||||
name = 'RemoteNotesCleaning1753863104203'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\'');
|
||||
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\'');
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningExpiryDaysForEachNotes"');
|
||||
await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningMaxProcessingDurationInMinutes"');
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableRemoteNotesCleaning"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RemoveNoteConstraints1753868431598 {
|
||||
name = 'RemoveNoteConstraints1753868431598'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class TweakDefaultFederationSettings1754019326356 {
|
||||
name = 'TweakDefaultFederationSettings1754019326356'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT true`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.18.1 || ^22.0.0"
|
||||
"node": "^22.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
|
||||
@@ -20,6 +20,8 @@ import { CacheService } from '@/core/CacheService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||
|
||||
type NoteFilter = (note: MiNote) => boolean;
|
||||
|
||||
type TimelineOptions = {
|
||||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
@@ -28,7 +30,7 @@ type TimelineOptions = {
|
||||
me?: { id: MiUser['id'] } | undefined | null,
|
||||
useDbFallback: boolean,
|
||||
redisTimelines: FanoutTimelineName[],
|
||||
noteFilter?: (note: MiNote) => boolean,
|
||||
noteFilter?: NoteFilter,
|
||||
alwaysIncludeMyNotes?: boolean;
|
||||
ignoreAuthorFromBlock?: boolean;
|
||||
ignoreAuthorFromMute?: boolean;
|
||||
@@ -79,7 +81,7 @@ export class FanoutTimelineEndpointService {
|
||||
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
let filter = ps.noteFilter ?? (_note => true);
|
||||
let filter = ps.noteFilter ?? (_note => true) as NoteFilter;
|
||||
|
||||
if (ps.alwaysIncludeMyNotes && ps.me) {
|
||||
const me = ps.me;
|
||||
@@ -145,15 +147,11 @@ export class FanoutTimelineEndpointService {
|
||||
{
|
||||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
const noteJoined = note as MiNote & {
|
||||
renoteUser: MiUser | null;
|
||||
replyUser: MiUser | null;
|
||||
};
|
||||
if (!ps.ignoreAuthorFromUserSuspension) {
|
||||
if (note.user!.isSuspended) return false;
|
||||
}
|
||||
if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false;
|
||||
if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false;
|
||||
if (note.userId !== note.renoteUserId && note.renote?.user?.isSuspended) return false;
|
||||
if (note.userId !== note.replyUserId && note.reply?.user?.isSuspended) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
@@ -200,7 +198,7 @@ export class FanoutTimelineEndpointService {
|
||||
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
|
||||
}
|
||||
|
||||
private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
|
||||
private async getAndFilterFromDb(noteIds: string[], noteFilter: NoteFilter, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
|
||||
@@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
emojis,
|
||||
userId: user.id,
|
||||
localOnly: data.localOnly!,
|
||||
reactionAcceptance: data.reactionAcceptance,
|
||||
reactionAcceptance: data.reactionAcceptance ?? null,
|
||||
visibility: data.visibility as any,
|
||||
visibleUserIds: data.visibility === 'specified'
|
||||
? data.visibleUsers
|
||||
@@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
await this.notesRepository.insert(insert);
|
||||
}
|
||||
|
||||
return insert;
|
||||
return {
|
||||
...insert,
|
||||
reply: data.reply ?? null,
|
||||
renote: data.renote ?? null,
|
||||
};
|
||||
} catch (e) {
|
||||
// duplicate key error
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
|
||||
@@ -62,7 +62,6 @@ export class NoteDeleteService {
|
||||
*/
|
||||
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
|
||||
const deletedAt = new Date();
|
||||
const cascadingNotes = await this.findCascadingNotes(note);
|
||||
|
||||
if (note.replyId) {
|
||||
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
|
||||
@@ -90,15 +89,6 @@ export class NoteDeleteService {
|
||||
|
||||
this.deliverToConcerned(user, note, content);
|
||||
}
|
||||
|
||||
// also deliver delete activity to cascaded notes
|
||||
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
|
||||
for (const cascadingNote of federatedLocalCascadingNotes) {
|
||||
if (!cascadingNote.user) continue;
|
||||
if (!this.userEntityService.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);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.notesChart.update(note, false);
|
||||
@@ -118,9 +108,6 @@ export class NoteDeleteService {
|
||||
}
|
||||
}
|
||||
|
||||
for (const cascadingNote of cascadingNotes) {
|
||||
this.searchService.unindexNote(cascadingNote);
|
||||
}
|
||||
this.searchService.unindexNote(note);
|
||||
|
||||
await this.notesRepository.delete({
|
||||
@@ -140,29 +127,6 @@ export class NoteDeleteService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async findCascadingNotes(note: MiNote): Promise<MiNote[]> {
|
||||
const recursive = async (noteId: string): Promise<MiNote[]> => {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.replyId = :noteId', { noteId })
|
||||
.orWhere(new Brackets(q => {
|
||||
q.where('note.renoteId = :noteId', { noteId })
|
||||
.andWhere('note.text IS NOT NULL');
|
||||
}))
|
||||
.leftJoinAndSelect('note.user', 'user');
|
||||
const replies = await query.getMany();
|
||||
|
||||
return [
|
||||
replies,
|
||||
...await Promise.all(replies.map(reply => recursive(reply.id))),
|
||||
].flat();
|
||||
};
|
||||
|
||||
const cascadingNotes: MiNote[] = await recursive(note.id);
|
||||
|
||||
return cascadingNotes;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getMentionedRemoteUsers(note: MiNote) {
|
||||
const where = [] as any[];
|
||||
|
||||
@@ -360,7 +360,7 @@ export class QueryService {
|
||||
public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
|
||||
if (excludeAuthor) {
|
||||
const brakets = (user: string) => new Brackets(qb => qb
|
||||
.where(`note.${user}Id IS NULL`)
|
||||
.where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮
|
||||
.orWhere(`user.id = ${user}.id`)
|
||||
.orWhere(`${user}.isSuspended = FALSE`));
|
||||
q
|
||||
@@ -368,7 +368,7 @@ export class QueryService {
|
||||
.andWhere(brakets('renoteUser'));
|
||||
} else {
|
||||
const brakets = (user: string) => new Brackets(qb => qb
|
||||
.where(`note.${user}Id IS NULL`)
|
||||
.where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮
|
||||
.orWhere(`${user}.isSuspended = FALSE`));
|
||||
q
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
|
||||
@@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
import { type SystemWebhookPayload } from '@/core/SystemWebhookService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { type UserWebhookPayload } from './UserWebhookService.js';
|
||||
import type {
|
||||
DbJobData,
|
||||
@@ -39,7 +40,6 @@ import type {
|
||||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
export const QUEUE_TYPES = [
|
||||
'system',
|
||||
@@ -53,6 +53,37 @@ export const QUEUE_TYPES = [
|
||||
'systemWebhookDeliver',
|
||||
] as const;
|
||||
|
||||
const REPEATABLE_SYSTEM_JOB_DEF = [{
|
||||
name: 'tickCharts',
|
||||
pattern: '55 * * * *',
|
||||
}, {
|
||||
name: 'resyncCharts',
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
name: 'cleanCharts',
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
name: 'aggregateRetention',
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
name: 'clean',
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
name: 'checkExpiredMutings',
|
||||
pattern: '*/5 * * * *',
|
||||
}, {
|
||||
name: 'bakeBufferedReactions',
|
||||
pattern: '0 0 * * *',
|
||||
}, {
|
||||
name: 'checkModeratorsActivity',
|
||||
// 毎時30分に起動
|
||||
pattern: '30 * * * *',
|
||||
}, {
|
||||
name: 'cleanRemoteNotes',
|
||||
// 毎日午前4時に起動(最も人の少ない時間帯)
|
||||
pattern: '0 4 * * *',
|
||||
}];
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
constructor(
|
||||
@@ -69,61 +100,30 @@ export class QueueService {
|
||||
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
|
||||
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
|
||||
) {
|
||||
this.systemQueue.add('tickCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '55 * * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
for (const def of REPEATABLE_SYSTEM_JOB_DEF) {
|
||||
this.systemQueue.upsertJobScheduler(def.name, {
|
||||
pattern: def.pattern,
|
||||
}, {
|
||||
name: def.name,
|
||||
opts: {
|
||||
// 期限ではなくcountで設定したいが、ジョブごとではなくキュー全体でカウントされるため、高頻度で実行されるジョブによって低頻度で実行されるジョブのログが消えることになる
|
||||
removeOnComplete: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
},
|
||||
removeOnFail: {
|
||||
age: 3600 * 24 * 7, // keep up to 7 days
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.systemQueue.add('resyncCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('cleanCharts', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('aggregateRetention', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('clean', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkExpiredMutings', {
|
||||
}, {
|
||||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('bakeBufferedReactions', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkModeratorsActivity', {
|
||||
}, {
|
||||
// 毎時30分に起動
|
||||
repeat: { pattern: '30 * * * *' },
|
||||
removeOnComplete: 10,
|
||||
removeOnFail: 30,
|
||||
// 古いバージョンで作成され現在使われなくなったrepeatableジョブをクリーンアップ
|
||||
this.systemQueue.getJobSchedulers().then(schedulers => {
|
||||
for (const scheduler of schedulers) {
|
||||
if (!REPEATABLE_SYSTEM_JOB_DEF.some(def => def.name === scheduler.key)) {
|
||||
this.systemQueue.removeJobScheduler(scheduler.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -810,6 +810,13 @@ export class QueueService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueGetJobLogs(queueType: typeof QUEUE_TYPES[number], jobId: string) {
|
||||
const queue = this.getQueue(queueType);
|
||||
const result = await queue.getJobLogs(jobId);
|
||||
return result.logs;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) {
|
||||
const RETURN_LIMIT = 100;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { EntityNotFoundError, In } from 'typeorm';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
@@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
|
||||
return appearNoteIds;
|
||||
}
|
||||
|
||||
async function nullIfEntityNotFound<T>(promise: Promise<T>): Promise<T | null> {
|
||||
try {
|
||||
return await promise;
|
||||
} catch (err) {
|
||||
if (err instanceof EntityNotFoundError) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NoteEntityService implements OnModuleInit {
|
||||
private userEntityService: UserEntityService;
|
||||
@@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit {
|
||||
...(opts.detail ? {
|
||||
clippedCount: note.clippedCount,
|
||||
|
||||
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
||||
// そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
|
||||
reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, {
|
||||
detail: false,
|
||||
skipHide: opts.skipHide,
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : undefined,
|
||||
})) : undefined,
|
||||
|
||||
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
|
||||
// そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
|
||||
renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(this.pack(note.renote ?? note.renoteId, me, {
|
||||
detail: true,
|
||||
skipHide: opts.skipHide,
|
||||
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
|
||||
_hint_: options?._hint_,
|
||||
}) : undefined,
|
||||
})) : undefined,
|
||||
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||
|
||||
@@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
private findNoteOrFail(id: string): Promise<MiNote> {
|
||||
return this.notesRepository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: ['user'],
|
||||
relations: ['user', 'renote', 'reply'],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -654,7 +654,7 @@ export class MiMeta {
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
default: 'all',
|
||||
default: 'none',
|
||||
})
|
||||
public federation: 'all' | 'specified' | 'none';
|
||||
|
||||
@@ -701,6 +701,21 @@ export class MiMeta {
|
||||
default: true,
|
||||
})
|
||||
public allowExternalApRedirect: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableRemoteNotesCleaning: boolean;
|
||||
|
||||
@Column('integer', {
|
||||
default: 60, // minutes
|
||||
})
|
||||
public remoteNotesCleaningMaxProcessingDurationInMinutes: number;
|
||||
|
||||
@Column('integer', {
|
||||
default: 90, // days
|
||||
})
|
||||
public remoteNotesCleaningExpiryDaysForEachNotes: number;
|
||||
}
|
||||
|
||||
export type SoftwareSuspension = {
|
||||
|
||||
@@ -36,7 +36,7 @@ export class MiNote {
|
||||
public replyId: MiNote['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
createForeignKeyConstraints: false,
|
||||
})
|
||||
@JoinColumn()
|
||||
public reply: MiNote | null;
|
||||
@@ -50,7 +50,7 @@ export class MiNote {
|
||||
public renoteId: MiNote['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
createForeignKeyConstraints: false,
|
||||
})
|
||||
@JoinColumn()
|
||||
public renote: MiNote | null;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||
@@ -18,6 +17,8 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
|
||||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
|
||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { CheckModeratorsActivityProcessorService } from './processors/CheckModeratorsActivityProcessorService.js';
|
||||
import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js';
|
||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
@@ -83,6 +84,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||
AggregateRetentionProcessorService,
|
||||
CheckExpiredMutingsProcessorService,
|
||||
CheckModeratorsActivityProcessorService,
|
||||
CleanRemoteNotesProcessorService,
|
||||
QueueProcessorService,
|
||||
],
|
||||
exports: [
|
||||
|
||||
@@ -43,6 +43,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
|
||||
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
|
||||
import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js';
|
||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||
import { QUEUE, baseWorkerOptions } from './const.js';
|
||||
|
||||
@@ -123,6 +124,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
||||
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
|
||||
private cleanProcessorService: CleanProcessorService,
|
||||
private cleanRemoteNotesProcessorService: CleanRemoteNotesProcessorService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger;
|
||||
|
||||
@@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
||||
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
|
||||
case 'clean': return this.cleanProcessorService.process();
|
||||
case 'cleanRemoteNotes': return this.cleanRemoteNotesProcessorService.process(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { And, In, IsNull, LessThan, MoreThan, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiMeta, MiNote, NoteFavoritesRepository, NotesRepository, UserNotePiningsRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class CleanRemoteNotesProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.noteFavoritesRepository)
|
||||
private noteFavoritesRepository: NoteFavoritesRepository,
|
||||
|
||||
@Inject(DI.userNotePiningsRepository)
|
||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-notes');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<Record<string, unknown>>): Promise<{
|
||||
deletedCount: number;
|
||||
oldest: number | null;
|
||||
newest: number | null;
|
||||
skipped?: boolean;
|
||||
}> {
|
||||
if (!this.meta.enableRemoteNotesCleaning) {
|
||||
this.logger.info('Remote notes cleaning is disabled, skipping...');
|
||||
return {
|
||||
deletedCount: 0,
|
||||
oldest: null,
|
||||
newest: null,
|
||||
skipped: true,
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.info('cleaning remote notes...');
|
||||
|
||||
const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds
|
||||
const startAt = Date.now();
|
||||
|
||||
const MAX_NOTE_COUNT_PER_QUERY = 50;
|
||||
|
||||
const stats = {
|
||||
deletedCount: 0,
|
||||
oldest: null as number | null,
|
||||
newest: null as number | null,
|
||||
};
|
||||
|
||||
let cursor: MiNote['id'] = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes));
|
||||
|
||||
while (true) {
|
||||
const batchBeginAt = Date.now();
|
||||
|
||||
let notes: Pick<MiNote, 'id'>[] = await this.notesRepository.find({
|
||||
where: {
|
||||
id: LessThan(cursor),
|
||||
userHost: Not(IsNull()),
|
||||
clippedCount: 0,
|
||||
renoteCount: 0,
|
||||
},
|
||||
take: MAX_NOTE_COUNT_PER_QUERY,
|
||||
order: {
|
||||
// 新しい順
|
||||
// https://github.com/misskey-dev/misskey/pull/16292#issuecomment-3139376314
|
||||
id: -1,
|
||||
},
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
const fetchedCount = notes.length;
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.id < cursor) {
|
||||
cursor = note.id;
|
||||
}
|
||||
}
|
||||
|
||||
const pinings = notes.length === 0 ? [] : await this.userNotePiningsRepository.find({
|
||||
where: {
|
||||
noteId: In(notes.map(note => note.id)),
|
||||
},
|
||||
select: ['noteId'],
|
||||
});
|
||||
|
||||
notes = notes.filter(note => {
|
||||
return !pinings.some(pining => pining.noteId === note.id);
|
||||
});
|
||||
|
||||
const favorites = notes.length === 0 ? [] : await this.noteFavoritesRepository.find({
|
||||
where: {
|
||||
noteId: In(notes.map(note => note.id)),
|
||||
},
|
||||
select: ['noteId'],
|
||||
});
|
||||
|
||||
notes = notes.filter(note => {
|
||||
return !favorites.some(favorite => favorite.noteId === note.id);
|
||||
});
|
||||
|
||||
const replies = notes.length === 0 ? [] : await this.notesRepository.find({
|
||||
where: {
|
||||
replyId: In(notes.map(note => note.id)),
|
||||
userHost: IsNull(),
|
||||
},
|
||||
select: ['replyId'],
|
||||
});
|
||||
|
||||
notes = notes.filter(note => {
|
||||
return !replies.some(reply => reply.replyId === note.id);
|
||||
});
|
||||
|
||||
if (notes.length > 0) {
|
||||
await this.notesRepository.delete(notes.map(note => note.id));
|
||||
|
||||
for (const note of notes) {
|
||||
const t = this.idService.parse(note.id).date.getTime();
|
||||
if (stats.oldest === null || t < stats.oldest) {
|
||||
stats.oldest = t;
|
||||
}
|
||||
if (stats.newest === null || t > stats.newest) {
|
||||
stats.newest = t;
|
||||
}
|
||||
}
|
||||
|
||||
stats.deletedCount += notes.length;
|
||||
}
|
||||
|
||||
job.log(`Deleted ${notes.length} of ${fetchedCount}; ${Date.now() - batchBeginAt}ms`);
|
||||
|
||||
const elapsed = Date.now() - startAt;
|
||||
|
||||
if (elapsed >= maxDuration) {
|
||||
this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`);
|
||||
job.log('Reached maximum duration, stopping cleaning.');
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
job.updateProgress((elapsed / maxDuration) * 100);
|
||||
|
||||
await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db
|
||||
}
|
||||
|
||||
this.logger.succ('cleaning of remote notes completed.');
|
||||
|
||||
return {
|
||||
deletedCount: stats.deletedCount,
|
||||
oldest: stats.oldest,
|
||||
newest: stats.newest,
|
||||
skipped: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,8 @@ export class GetterService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getNoteWithUser(noteId: MiNote['id']) {
|
||||
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
|
||||
public async getNoteWithRelations(noteId: MiNote['id']) {
|
||||
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] });
|
||||
|
||||
if (note == null) {
|
||||
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
||||
|
||||
@@ -70,6 +70,7 @@ export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-dela
|
||||
export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js';
|
||||
export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js';
|
||||
export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js';
|
||||
export * as 'admin/queue/show-job-logs' from './endpoints/admin/queue/show-job-logs.js';
|
||||
export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js';
|
||||
export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js';
|
||||
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
|
||||
|
||||
@@ -571,6 +571,18 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableRemoteNotesCleaning: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -722,6 +734,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
proxyRemoteFiles: instance.proxyRemoteFiles,
|
||||
signToActivityPubGet: instance.signToActivityPubGet,
|
||||
allowExternalApRedirect: instance.allowExternalApRedirect,
|
||||
enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning,
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
kind: 'read:admin:queue',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
optional: false, nullable: false,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
queue: { type: 'string', enum: QUEUE_TYPES },
|
||||
jobId: { type: 'string' },
|
||||
},
|
||||
required: ['queue', 'jobId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
return this.queueService.queueGetJobLogs(ps.queue, ps.jobId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,9 @@ export const paramDef = {
|
||||
proxyRemoteFiles: { type: 'boolean' },
|
||||
signToActivityPubGet: { type: 'boolean' },
|
||||
allowExternalApRedirect: { type: 'boolean' },
|
||||
enableRemoteNotesCleaning: { type: 'boolean' },
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -723,6 +726,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.allowExternalApRedirect = ps.allowExternalApRedirect;
|
||||
}
|
||||
|
||||
if (ps.enableRemoteNotesCleaning !== undefined) {
|
||||
set.enableRemoteNotesCleaning = ps.enableRemoteNotesCleaning;
|
||||
}
|
||||
|
||||
if (ps.remoteNotesCleaningExpiryDaysForEachNotes !== undefined) {
|
||||
set.remoteNotesCleaningExpiryDaysForEachNotes = ps.remoteNotesCleaningExpiryDaysForEachNotes;
|
||||
}
|
||||
|
||||
if (ps.remoteNotesCleaningMaxProcessingDurationInMinutes !== undefined) {
|
||||
set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes;
|
||||
}
|
||||
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update(set);
|
||||
|
||||
@@ -269,7 +269,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
let renote: MiNote | null = null;
|
||||
if (ps.renoteId != null) {
|
||||
// Fetch renote to note
|
||||
renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
|
||||
renote = await this.notesRepository.findOne({
|
||||
where: { id: ps.renoteId },
|
||||
relations: ['user', 'renote', 'reply'],
|
||||
});
|
||||
|
||||
if (renote == null) {
|
||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
@@ -315,7 +318,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
let reply: MiNote | null = null;
|
||||
if (ps.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await this.notesRepository.findOneBy({ id: ps.replyId });
|
||||
reply = await this.notesRepository.findOne({
|
||||
where: { id: ps.replyId },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (reply == null) {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private getterService: GetterService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => {
|
||||
const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => {
|
||||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw err;
|
||||
});
|
||||
|
||||
@@ -237,7 +237,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere('note.renoteId IS NULL');
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ export class ClientServerService {
|
||||
id: request.params.note,
|
||||
visibility: In(['public', 'home']),
|
||||
},
|
||||
relations: ['user'],
|
||||
relations: ['user', 'reply', 'renote'],
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -821,8 +821,11 @@ export class ClientServerService {
|
||||
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
|
||||
reply.removeHeader('X-Frame-Options');
|
||||
|
||||
const note = await this.notesRepository.findOneBy({
|
||||
id: request.params.note,
|
||||
const note = await this.notesRepository.findOne({
|
||||
where: {
|
||||
id: request.params.note,
|
||||
},
|
||||
relations: ['user', 'reply', 'renote'],
|
||||
});
|
||||
|
||||
if (note == null) return;
|
||||
|
||||
@@ -63,7 +63,6 @@ describe('Note', () => {
|
||||
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||
'id',
|
||||
'emojis',
|
||||
'reactionAcceptance',
|
||||
'replyId',
|
||||
'reply',
|
||||
'userId',
|
||||
@@ -105,7 +104,6 @@ describe('Note', () => {
|
||||
deepStrictEqualWithExcludedFields(note, resolvedNote, [
|
||||
'id',
|
||||
'emojis',
|
||||
'reactionAcceptance',
|
||||
'renoteId',
|
||||
'renote',
|
||||
'userId',
|
||||
|
||||
@@ -673,7 +673,6 @@ describe('アンテナ', () => {
|
||||
assert.deepStrictEqual(response, expected);
|
||||
});
|
||||
|
||||
|
||||
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
|
||||
test.each([
|
||||
{ label: 'ID指定', offsetBy: 'id' },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -145,7 +145,7 @@ import { claimAchievement } from '@/utility/achievements.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { chooseFileFromPcAndUpload, selectDriveFolder } from '@/utility/drive.js';
|
||||
import { store } from '@/store.js';
|
||||
import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
|
||||
import { makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
|
||||
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
|
||||
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
|
||||
|
||||
@@ -52,15 +52,20 @@ import TestWebGL2 from '@/workers/test-webgl2?worker';
|
||||
import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
|
||||
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
|
||||
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null);
|
||||
|
||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
|
||||
// テスト環境で Web Worker インスタンスは作成できない
|
||||
if (import.meta.env.MODE === 'test') {
|
||||
if (isTest) {
|
||||
const canvas = window.document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
resolve(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
const testWorker = new TestWebGL2();
|
||||
testWorker.addEventListener('message', event => {
|
||||
if (event.data.result) {
|
||||
@@ -189,7 +194,7 @@ function drawAvg() {
|
||||
}
|
||||
|
||||
async function draw() {
|
||||
if (import.meta.env.MODE === 'test' && props.hash == null) return;
|
||||
if (isTest && props.hash == null) return;
|
||||
|
||||
drawAvg();
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
||||
tabindex="0"
|
||||
>
|
||||
<MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
|
||||
<div v-if="isRenote" :class="$style.renote">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
@@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="isEnabledUrlPreview">
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||
</div>
|
||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
@@ -282,7 +282,7 @@ let note = deepClone(props.note);
|
||||
//}
|
||||
|
||||
const isRenote = Misskey.note.isPureRenote(note);
|
||||
const appearNote = getAppearNote(note);
|
||||
const appearNote = getAppearNote(note) ?? note;
|
||||
const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({
|
||||
note: appearNote,
|
||||
parentNote: note,
|
||||
|
||||
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
|
||||
</div>
|
||||
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<MkNoteSub v-if="appearNote.replyId" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="isRenote" :class="$style.renote">
|
||||
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
|
||||
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div v-if="note" :class="$style.root">
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
@@ -19,6 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.deleted">
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -27,9 +30,10 @@ import * as Misskey from 'misskey-js';
|
||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note | null;
|
||||
}>();
|
||||
|
||||
const showContent = ref(false);
|
||||
@@ -101,4 +105,14 @@ const showContent = ref(false);
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted {
|
||||
text-align: center;
|
||||
padding: 8px !important;
|
||||
margin: 8px 8px 0 8px;
|
||||
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="!muted" :class="[$style.root, { [$style.children]: depth > 1 }]">
|
||||
<div v-if="note == null" :class="$style.deleted">
|
||||
{{ i18n.ts.deletedNote }}
|
||||
</div>
|
||||
<div v-else-if="!muted" :class="[$style.root, { [$style.children]: depth > 1 }]">
|
||||
<div :class="$style.main">
|
||||
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
|
||||
@@ -53,7 +56,7 @@ import { userPage } from '@/filters/user.js';
|
||||
import { checkWordMute } from '@/utility/check-word-mute.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
note: Misskey.entities.Note | null;
|
||||
detail?: boolean;
|
||||
|
||||
// how many notes are in between this one and the note being viewed in detail
|
||||
@@ -62,12 +65,12 @@ const props = withDefaults(defineProps<{
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||
const muted = ref(props.note && $i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
|
||||
|
||||
const showContent = ref(false);
|
||||
const replies = ref<Misskey.entities.Note[]>([]);
|
||||
|
||||
if (props.detail) {
|
||||
if (props.detail && props.note) {
|
||||
misskeyApi('notes/children', {
|
||||
noteId: props.note.id,
|
||||
limit: 5,
|
||||
@@ -160,4 +163,14 @@ if (props.detail) {
|
||||
margin: 8px 8px 0 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
text-align: center;
|
||||
padding: 8px !important;
|
||||
margin: 8px 8px 0 8px;
|
||||
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,15 +10,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
|
||||
<template v-for="(note, i) in notes" :key="note.id">
|
||||
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
|
||||
<div :class="$style.date">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
|
||||
<div
|
||||
v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i - 1].createdAt, note.createdAt)"
|
||||
:data-scroll-anchor="note.id"
|
||||
:class="{ '_gaps': !noGap }"
|
||||
>
|
||||
<div :class="[$style.date, { [$style.noGap]: noGap }]">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i - 1].createdAt, note.createdAt)?.prevText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i - 1].createdAt, note.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
</div>
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<div v-if="note._shouldInsertAd_" :class="$style.ad">
|
||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id">
|
||||
<div v-else-if="note._shouldInsertAd_" :class="{ '_gaps': !noGap }" :data-scroll-anchor="note.id">
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<div :class="$style.ad">
|
||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||
@@ -103,7 +110,10 @@ defineExpose({
|
||||
opacity: 0.75;
|
||||
padding: 8px 8px;
|
||||
margin: 0 auto;
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
|
||||
&.noGap {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
}
|
||||
|
||||
.ad:empty {
|
||||
|
||||
@@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-planet"></i></template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}</div>
|
||||
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
|
||||
|
||||
<MkRadios v-model="q_federation" :vertical="true">
|
||||
<option value="yes">{{ i18n.ts.yes }}</option>
|
||||
@@ -63,6 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkRadios>
|
||||
|
||||
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
|
||||
|
||||
<MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning">
|
||||
<template #label>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning }}</template>
|
||||
<template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }}</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
@@ -110,6 +115,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div><b>{{ i18n.ts.federation }}:</b></div>
|
||||
<div>{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><b>{{ i18n.ts._serverSettings.remoteNotesCleaning }}:</b></div>
|
||||
<div>{{ serverSettings.enableRemoteNotesCleaning ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div><b>FTT:</b></div>
|
||||
<div>{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}</div>
|
||||
@@ -185,7 +194,9 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'finished'): void;
|
||||
@@ -200,6 +211,7 @@ const q_name = ref('');
|
||||
const q_use = ref('single');
|
||||
const q_scale = ref('small');
|
||||
const q_federation = ref('yes');
|
||||
const q_remoteContentsCleaning = ref(true);
|
||||
const q_adminName = ref('');
|
||||
const q_adminEmail = ref('');
|
||||
|
||||
@@ -217,6 +229,7 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
|
||||
emailRequiredForSignup: q_use.value === 'open',
|
||||
enableIpLogging: q_use.value === 'open',
|
||||
federation: q_federation.value === 'yes' ? 'all' : 'none',
|
||||
enableRemoteNotesCleaning: q_remoteContentsCleaning.value,
|
||||
enableFanoutTimeline: true,
|
||||
enableFanoutTimelineDbFallback: q_use.value === 'single',
|
||||
enableReactionsBuffering,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="windowEl"
|
||||
:withOkButton="false"
|
||||
:okButtonDisabled="false"
|
||||
:width="500"
|
||||
:height="600"
|
||||
@close="onCloseModalWindow"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>Server setup wizard</template>
|
||||
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<MkServerSetupWizard @finished="onWizardFinished"/>
|
||||
</template>
|
||||
<template #fallback>
|
||||
<MkLoading/>
|
||||
</template>
|
||||
</Suspense>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkServerSetupWizard from '@/components/MkServerSetupWizard.vue';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'),
|
||||
}>();
|
||||
|
||||
const windowEl = useTemplateRef('windowEl');
|
||||
|
||||
function onWizardFinished() {
|
||||
windowEl.value?.close();
|
||||
}
|
||||
|
||||
function onCloseModalWindow() {
|
||||
windowEl.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
max-height: 410px;
|
||||
height: 410px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template v-for="(note, i) in paginator.items.value" :key="note.id">
|
||||
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
|
||||
<div :class="$style.date">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span>
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.prevText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
</div>
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</div>
|
||||
|
||||
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<div v-for="(notification, i) in paginator.items.value" :key="notification.id" :data-scroll-anchor="notification.id" :class="$style.item">
|
||||
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, notification.createdAt)" :class="$style.date">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).prevText }}</span>
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt)?.prevText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
|
||||
</div>
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.content" :note="notification.note" :withHardMute="true"/>
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type) && 'note' in notification" :class="$style.content" :note="notification.note" :withHardMute="true"/>
|
||||
<XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
@@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkKeyValue>
|
||||
<MkKeyValue v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0">
|
||||
<template #key>Progress</template>
|
||||
<template #value>{{ Math.floor(job.progress * 100) }}%</template>
|
||||
<template #value>{{ Math.floor(job.progress) }}%</template>
|
||||
</MkKeyValue>
|
||||
</div>
|
||||
<MkFolder :withSpacer="false">
|
||||
@@ -150,11 +150,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton><i class="ti ti-device-floppy"></i> Update</MkButton>
|
||||
</div>
|
||||
<div v-else-if="tab === 'result'">
|
||||
<MkCode :code="String(job.returnValue)"/>
|
||||
<MkCode :code="JSON5.stringify(job.returnValue, null, '\t')" lang="json5"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'error'" class="_gaps_s">
|
||||
<MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'logs'">
|
||||
<MkButton primary rounded @click="loadLogs()"><i class="ti ti-refresh"></i> Load logs</MkButton>
|
||||
<div v-for="log in logs">{{ log }}</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
||||
@@ -198,6 +202,7 @@ const emit = defineEmits<{
|
||||
const tab = ref('info');
|
||||
const editData = ref(JSON5.stringify(props.job.data, null, '\t'));
|
||||
const canEdit = true;
|
||||
const logs = ref<string[]>([]);
|
||||
|
||||
type TlType = TlEvent<{
|
||||
type: 'created' | 'processed' | 'finished';
|
||||
@@ -268,6 +273,10 @@ async function removeJob() {
|
||||
os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id });
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
logs.value = await os.apiWithDialog('admin/queue/show-job-logs', { queue: props.queueType, jobId: props.job.id });
|
||||
}
|
||||
|
||||
// TODO
|
||||
// function moveJob() {
|
||||
//
|
||||
|
||||
@@ -101,6 +101,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #icon><i class="ti ti-recycle"></i></template>
|
||||
<template #label>Remote Notes Cleaning (仮)</template>
|
||||
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
|
||||
<template v-else #suffix>Disabled</template>
|
||||
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
|
||||
<MkFormFooter :form="remoteNotesCleaningForm"/>
|
||||
</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
|
||||
<template #label>{{ i18n.ts.enable }}<span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<template #caption>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
|
||||
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
|
||||
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }} ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<template #suffix>{{ i18n.ts._time.day }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
|
||||
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }} ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||
</MkInput>
|
||||
</template>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
@@ -196,6 +225,19 @@ const rbtForm = useForm({
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const remoteNotesCleaningForm = useForm({
|
||||
enableRemoteNotesCleaning: meta.enableRemoteNotesCleaning,
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: meta.remoteNotesCleaningExpiryDaysForEachNotes,
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: meta.remoteNotesCleaningMaxProcessingDurationInMinutes,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
enableRemoteNotesCleaning: state.enableRemoteNotesCleaning,
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: state.remoteNotesCleaningExpiryDaysForEachNotes,
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: state.remoteNotesCleaningMaxProcessingDurationInMinutes,
|
||||
});
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
@@ -287,6 +287,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkButton primary @click="openSetupWizard">
|
||||
Open setup wizard
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithHeader>
|
||||
@@ -425,6 +429,20 @@ const proxyAccountForm = useForm({
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
async function openSetupWizard() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._serverSettings.restartServerSetupWizardConfirm_title,
|
||||
text: i18n.ts._serverSettings.restartServerSetupWizardConfirm_text,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkServerSetupWizardDialog.vue').then(x => x.default), {
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePage(() => ({
|
||||
|
||||
@@ -366,6 +366,7 @@ definePage(() => ({
|
||||
|
||||
> .items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
|
||||
@@ -87,7 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>{{ i18n.ts._serverSetupWizard.settingsYouMakeHereCanBeChangedLater }}</div>
|
||||
</div>
|
||||
|
||||
<MkServerSetupWizard :token="token" @finished="onWizardFinished"/>
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<MkServerSetupWizard :token="token" @finished="onWizardFinished"/>
|
||||
</template>
|
||||
<template #fallback>
|
||||
<MkLoading/>
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
<MkButton rounded style="margin: 0 auto;" @click="skipSettings">
|
||||
{{ i18n.ts._serverSetupWizard.skipSettings }}
|
||||
|
||||
@@ -64,12 +64,6 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
html._themeChangingFallback_ {
|
||||
&, * {
|
||||
transition: background 0.5s ease, border 0.5s ease !important;
|
||||
}
|
||||
}
|
||||
|
||||
html._themeChanging_ {
|
||||
view-transition-name: theme-changing;
|
||||
}
|
||||
|
||||
@@ -137,9 +137,10 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
}
|
||||
|
||||
if (deepEqual(currentTheme, theme)) return;
|
||||
currentTheme = theme;
|
||||
// リアクティビティ解除
|
||||
currentTheme = deepClone(theme);
|
||||
|
||||
if (window.document.startViewTransition != null && prefer.s.animation) {
|
||||
if (window.document.startViewTransition != null) {
|
||||
window.document.documentElement.classList.add('_themeChanging_');
|
||||
window.document.startViewTransition(async () => {
|
||||
applyThemeInternal(theme, persist);
|
||||
@@ -150,15 +151,9 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
globalEvents.emit('themeChanged');
|
||||
});
|
||||
} else {
|
||||
// TODO: ViewTransition API が主要ブラウザで対応したら消す
|
||||
window.document.documentElement.classList.add('_themeChangingFallback_');
|
||||
timeout = window.setTimeout(() => {
|
||||
window.document.documentElement.classList.remove('_themeChangingFallback_');
|
||||
// 色計算など再度行えるようにクライアント全体に通知
|
||||
globalEvents.emit('themeChanged');
|
||||
}, 500);
|
||||
|
||||
applyThemeInternal(theme, persist);
|
||||
// 色計算など再度行えるようにクライアント全体に通知
|
||||
globalEvents.emit('themeChanged');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ for (const item of generated) {
|
||||
const inline = rootMods.get(id);
|
||||
if (inline) {
|
||||
inline.parentId = item.id;
|
||||
inline.path = item.path;
|
||||
} else {
|
||||
console.log('[Settings Search Index] Failed to inline', id);
|
||||
}
|
||||
|
||||
@@ -296,6 +296,12 @@ type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requ
|
||||
// @public (undocumented)
|
||||
type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminQueueShowJobLogsRequest = operations['admin___queue___show-job-logs']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminQueueShowJobLogsResponse = operations['admin___queue___show-job-logs']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
|
||||
|
||||
@@ -1559,6 +1565,8 @@ declare namespace entities {
|
||||
AdminQueueRetryJobRequest,
|
||||
AdminQueueShowJobRequest,
|
||||
AdminQueueShowJobResponse,
|
||||
AdminQueueShowJobLogsRequest,
|
||||
AdminQueueShowJobLogsResponse,
|
||||
AdminQueueStatsResponse,
|
||||
AdminRelaysAddRequest,
|
||||
AdminRelaysAddResponse,
|
||||
@@ -3271,7 +3279,7 @@ type PromoReadRequest = operations['promo___read']['requestBody']['content']['ap
|
||||
type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text' | 'cw' | 'files' | 'fileIds' | 'poll'> & AllNullRecord<Pick<Note, 'text'>> & AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>> & {
|
||||
files: [];
|
||||
fileIds: [];
|
||||
} & NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||
} & NonNullableRecord<Pick<Note, 'renoteId'>> & Pick<Note, 'renote'>;
|
||||
|
||||
// @public (undocumented)
|
||||
type QueueCount = components['schemas']['QueueCount'];
|
||||
@@ -3801,7 +3809,7 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/entities.ts:54:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/entities.ts:55:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.7.0",
|
||||
"version": "2025.8.0-alpha.1",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
@@ -713,6 +713,17 @@ declare module '../api.js' {
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:queue*
|
||||
*/
|
||||
request<E extends 'admin/queue/show-job-logs', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
||||
@@ -88,6 +88,8 @@ import type {
|
||||
AdminQueueRetryJobRequest,
|
||||
AdminQueueShowJobRequest,
|
||||
AdminQueueShowJobResponse,
|
||||
AdminQueueShowJobLogsRequest,
|
||||
AdminQueueShowJobLogsResponse,
|
||||
AdminQueueStatsResponse,
|
||||
AdminRelaysAddRequest,
|
||||
AdminRelaysAddResponse,
|
||||
@@ -717,6 +719,7 @@ export type Endpoints = {
|
||||
'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse };
|
||||
'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse };
|
||||
'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse };
|
||||
'admin/queue/show-job-logs': { req: AdminQueueShowJobLogsRequest; res: AdminQueueShowJobLogsResponse };
|
||||
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
|
||||
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
|
||||
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };
|
||||
|
||||
@@ -91,6 +91,8 @@ export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job'
|
||||
export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
|
||||
export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
|
||||
export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];
|
||||
export type AdminQueueShowJobLogsRequest = operations['admin___queue___show-job-logs']['requestBody']['content']['application/json'];
|
||||
export type AdminQueueShowJobLogsResponse = operations['admin___queue___show-job-logs']['responses']['200']['content']['application/json'];
|
||||
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
|
||||
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
|
||||
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];
|
||||
|
||||
@@ -584,6 +584,15 @@ export type paths = {
|
||||
*/
|
||||
post: operations['admin___queue___show-job'];
|
||||
};
|
||||
'/admin/queue/show-job-logs': {
|
||||
/**
|
||||
* admin/queue/show-job-logs
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:queue*
|
||||
*/
|
||||
post: operations['admin___queue___show-job-logs'];
|
||||
};
|
||||
'/admin/queue/stats': {
|
||||
/**
|
||||
* admin/queue/stats
|
||||
@@ -9370,6 +9379,9 @@ export interface operations {
|
||||
proxyRemoteFiles: boolean;
|
||||
signToActivityPubGet: boolean;
|
||||
allowExternalApRedirect: boolean;
|
||||
enableRemoteNotesCleaning: boolean;
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: number;
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -10164,6 +10176,73 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
'admin___queue___show-job-logs': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @enum {string} */
|
||||
queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
|
||||
jobId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': string[];
|
||||
};
|
||||
};
|
||||
/** @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___queue___stats: {
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
@@ -12599,6 +12678,9 @@ export interface operations {
|
||||
proxyRemoteFiles?: boolean;
|
||||
signToActivityPubGet?: boolean;
|
||||
allowExternalApRedirect?: boolean;
|
||||
enableRemoteNotesCleaning?: boolean;
|
||||
remoteNotesCleaningExpiryDaysForEachNotes?: number;
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,7 +33,8 @@ export type PureRenote =
|
||||
& AllNullRecord<Pick<Note, 'text'>>
|
||||
& AllNullOrOptionalRecord<Pick<Note, 'reply' | 'replyId' | 'cw' | 'poll'>>
|
||||
& { files: []; fileIds: []; }
|
||||
& NonNullableRecord<Pick<Note, 'renote' | 'renoteId'>>;
|
||||
& NonNullableRecord<Pick<Note, 'renoteId'>>
|
||||
& Pick<Note, 'renote'>; // リノート対象が削除された場合、renoteIdはあるがrenoteはnullになる
|
||||
|
||||
export type PageEvent = {
|
||||
pageId: Page['id'];
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { Note, PureRenote } from './entities.js';
|
||||
|
||||
export function isPureRenote(note: Note): note is PureRenote {
|
||||
return (
|
||||
note.renote != null &&
|
||||
note.reply == null &&
|
||||
note.renoteId != null &&
|
||||
note.replyId == null &&
|
||||
note.text == null &&
|
||||
note.cw == null &&
|
||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
||||
|
||||
Reference in New Issue
Block a user