wip: auto note removal
This commit is contained in:
parent
f30c95e51a
commit
74c7b5fe70
@ -69,6 +69,12 @@ export class QueueService {
|
||||
repeat: { pattern: '*/5 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('autoNoteRemoval', {
|
||||
}, {
|
||||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AutoRemovalConditionRepository, MiAutoRemovalCondition } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class AutoRemovalConditionEntityService {
|
||||
constructor(
|
||||
@Inject(DI.autoRemovalConditionRepository)
|
||||
private autoRemovalConditionRepository: AutoRemovalConditionRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: MiAutoRemovalCondition['id'] | MiAutoRemovalCondition,
|
||||
): Promise<Packed<'AutoRemovalCondition'>> {
|
||||
const condition = typeof src === 'object' ? src : await this.autoRemovalConditionRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: condition.id,
|
||||
deleteAfter: condition.deleteAfter,
|
||||
noPiningNotes: condition.noPiningNotes,
|
||||
noSpecifiedNotes: condition.noSpecifiedNotes,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
conditions: any[],
|
||||
) {
|
||||
return Promise.all(conditions.map(x => this.pack(x)));
|
||||
}
|
||||
}
|
||||
|
@ -84,5 +84,6 @@ export const DI = {
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
autoRemovalConditionRepository: Symbol('autoRemovalConditionRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
import { packedAbuseUserReportSchema } from '@/models/json-schema/abuse-user-report.js';
|
||||
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
|
||||
import { packedAppSchema } from '@/models/json-schema/app.js';
|
||||
import { packedAutoRemovalConditionSchema } from '@/models/json-schema/auto-removal-condition.js';
|
||||
import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
|
||||
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
||||
import { packedClipSchema } from '@/models/json-schema/clip.js';
|
||||
@ -101,6 +102,7 @@ export const refs = {
|
||||
EmojiDetailed: packedEmojiDetailedSchema,
|
||||
Flash: packedFlashSchema,
|
||||
FlashLike: packedFlashLikeSchema,
|
||||
AutoRemovalCondition: packedAutoRemovalConditionSchema,
|
||||
|
||||
Signin: packedSigninSchema,
|
||||
RoleCondFormulaLogics: packedRoleCondFormulaLogicsSchema,
|
||||
|
40
packages/backend/src/models/AutoRemovalCondition.ts
Normal file
40
packages/backend/src/models/AutoRemovalCondition.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and noridev and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity, Column, PrimaryColumn, Index } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
|
||||
@Entity('auto_removal_condition')
|
||||
// @Index(['userId'], { unique: true })
|
||||
export class MiAutoRemovalCondition {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('bigint', {
|
||||
default: 7,
|
||||
nullable: false,
|
||||
})
|
||||
public deleteAfter: number;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
nullable: false,
|
||||
})
|
||||
public noPiningNotes: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
nullable: false,
|
||||
})
|
||||
public noSpecifiedNotes: boolean;
|
||||
|
||||
constructor(data: Partial<MiAutoRemovalCondition>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import {
|
||||
MiApp,
|
||||
MiAuthSession,
|
||||
MiAvatarDecoration,
|
||||
MiAutoRemovalCondition,
|
||||
MiBlocking,
|
||||
MiChannel,
|
||||
MiChannelFavorite,
|
||||
@ -314,6 +315,12 @@ const $authSessionsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $autoRemovalConditionRepository: Provider = {
|
||||
provide: DI.autoRemovalConditionRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAutoRemovalCondition),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $accessTokensRepository: Provider = {
|
||||
provide: DI.accessTokensRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAccessToken),
|
||||
@ -574,6 +581,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$abuseReportResolversRepository,
|
||||
$autoRemovalConditionRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
@ -646,6 +654,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$abuseReportResolversRepository,
|
||||
$autoRemovalConditionRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
@ -6,6 +6,7 @@
|
||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
import { MiAutoRemovalCondition } from './AutoRemovalCondition.js';
|
||||
|
||||
@Entity('user')
|
||||
@Index(['usernameLower', 'host'], { unique: true })
|
||||
@ -217,6 +218,12 @@ export class MiUser {
|
||||
})
|
||||
public isDeleted: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the User is using note auto removal.',
|
||||
})
|
||||
public autoRemoval: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
})
|
||||
@ -267,6 +274,18 @@ export class MiUser {
|
||||
})
|
||||
public token: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: false,
|
||||
})
|
||||
public autoRemovalConditionId: MiAutoRemovalCondition['id'];
|
||||
|
||||
@OneToOne(type => MiAutoRemovalCondition, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public autoRemovalCondition: MiAutoRemovalCondition;
|
||||
|
||||
constructor(data: Partial<MiUser>) {
|
||||
if (data == null) return;
|
||||
|
||||
@ -304,3 +323,4 @@ export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as con
|
||||
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||
|
||||
|
@ -13,6 +13,7 @@ import { MiAntenna } from '@/models/Antenna.js';
|
||||
import { MiApp } from '@/models/App.js';
|
||||
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
|
||||
import { MiAuthSession } from '@/models/AuthSession.js';
|
||||
import { MiAutoRemovalCondition } from '@/models/AutoRemovalCondition.js';
|
||||
import { MiBlocking } from '@/models/Blocking.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
@ -86,6 +87,7 @@ export {
|
||||
MiApp,
|
||||
MiAvatarDecoration,
|
||||
MiAuthSession,
|
||||
MiAutoRemovalCondition,
|
||||
MiBlocking,
|
||||
MiChannelFollowing,
|
||||
MiChannelFavorite,
|
||||
@ -158,6 +160,7 @@ export type AntennasRepository = Repository<MiAntenna>;
|
||||
export type AppsRepository = Repository<MiApp>;
|
||||
export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>;
|
||||
export type AuthSessionsRepository = Repository<MiAuthSession>;
|
||||
export type AutoRemovalConditionRepository = Repository<MiAutoRemovalCondition>;
|
||||
export type BlockingsRepository = Repository<MiBlocking>;
|
||||
export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;
|
||||
export type ChannelFavoritesRepository = Repository<MiChannelFavorite>;
|
||||
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedAutoRemovalConditionSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
deleteAfter: {
|
||||
type: 'number',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
noPiningNotes: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
noSpecifiedNotes: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
@ -83,6 +83,7 @@ import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { MiAutoRemovalCondition } from '@/models/AutoRemovalCondition.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@ -205,6 +206,7 @@ export const entities = [
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
MiAutoRemovalCondition,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { And, In, MoreThan, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUserNotePining, NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AutoNoteRemovalProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
private usersRepository: UsersRepository,
|
||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private noteDeleteService: NoteDeleteService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('auto-note-removal');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(): Promise<void> {
|
||||
this.logger.info('Checking notes that to remove automatically...');
|
||||
this.logger.info('Checking users that enabled note auto-removal');
|
||||
const users = await this.usersRepository.find({ where: { autoRemoval: true } });
|
||||
if (users.length < 1) {
|
||||
this.logger.info('Does not have any user that enabled autoRemoval');
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
|
||||
for (const user of users) {
|
||||
if (user.autoRemovalCondition === null) {
|
||||
continue;
|
||||
}
|
||||
const pinings: MiUserNotePining[] = await this.userNotePiningsRepository.findBy({ userId: user.id });
|
||||
const piningNoteIds: string[] = pinings.map(pining => pining.noteId); // pining.note always undefined (bug?)
|
||||
|
||||
const specifiedNotes: MiNote[] = await this.notesRepository.findBy({
|
||||
userId: user.id,
|
||||
visibility: Not(In(['public', 'home', 'followers'])),
|
||||
});
|
||||
const specifiedNoteIds: string[] = specifiedNotes.map(note => note.id);
|
||||
const deleteAfter: number = user.autoRemovalCondition.deleteAfter * 86400000;
|
||||
|
||||
// Delete notes
|
||||
let cursor: MiNote['id'] | null = null;
|
||||
let condition: string[] = [];
|
||||
if (user.autoRemovalCondition.noSpecifiedNotes === true) {
|
||||
condition = [...condition, ...specifiedNoteIds];
|
||||
}
|
||||
|
||||
if (user.autoRemovalCondition.noPiningNotes === true) {
|
||||
condition = [...condition, ...piningNoteIds];
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const notes = await this.notesRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? {
|
||||
id: And(Not(In(condition)), MoreThan(cursor)),
|
||||
} : {
|
||||
id: Not(In(condition)),
|
||||
}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as MiNote[];
|
||||
|
||||
if (notes.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = notes.at(-1)?.id ?? null;
|
||||
|
||||
for (const note of notes) {
|
||||
const createdAt: number = this.idService.parse(note.id).date.getTime();
|
||||
const delta: number = now - createdAt;
|
||||
if (delta > deleteAfter) {
|
||||
await Promise.bind(this.noteDeleteService.delete(user, note, false, user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.succ('All of auto-removable notes deleted');
|
||||
}
|
||||
|
||||
this.logger.succ('All notes to auto-remove has beed removed.');
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository, AutoRemovalConditionRepository } from '@/models/_.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
@ -112,7 +112,7 @@ export const meta = {
|
||||
},
|
||||
|
||||
uriNull: {
|
||||
message: 'User ActivityPup URI is null.',
|
||||
message: 'User ActivityPub URI is null.',
|
||||
code: 'URI_NULL',
|
||||
id: 'bf326f31-d430-4f97-9933-5d61e4d48a23',
|
||||
},
|
||||
@ -257,6 +257,8 @@ export const paramDef = {
|
||||
required: ['mutualLinks'],
|
||||
},
|
||||
},
|
||||
autoRemoval: { type: 'boolean' },
|
||||
autoRemovalCondition: { type: 'object' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
@ -278,6 +280,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
@Inject(DI.pagesRepository)
|
||||
private pagesRepository: PagesRepository,
|
||||
|
||||
@Inject(DI.autoRemovalConditionRepository)
|
||||
private autoRemovalConditionRepository: AutoRemovalConditionRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
@ -345,6 +350,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (typeof ps.isVacation === 'boolean') updates.isVacation = ps.isVacation;
|
||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||
if (typeof ps.autoRemoval === 'boolean') updates.autoRemoval = ps.autoRemoval;
|
||||
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||
if (policy.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
||||
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||
@ -521,6 +527,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id });
|
||||
}
|
||||
|
||||
if (ps.autoRemovalCondition !== undefined) {
|
||||
await this.autoRemovalConditionRepository.update(user.autoRemovalConditionId, ps.autoRemovalCondition);
|
||||
}
|
||||
|
||||
await this.userProfilesRepository.update(user.id, {
|
||||
...profileUpdates,
|
||||
verifiedLinks: [],
|
||||
|
Loading…
Reference in New Issue
Block a user