From db583cedc6b157b46b44b8766467a7b16f11cb60 Mon Sep 17 00:00:00 2001 From: caipira113 Date: Sat, 7 Oct 2023 20:03:52 +0900 Subject: [PATCH 1/5] 11111 11111 --- .../migration/1696604572677-poll_vote_poll.js | 12 + packages/backend/src/core/CoreModule.ts | 6 + .../backend/src/core/GlobalEventService.ts | 2 +- .../backend/src/core/NoteCreateService.ts | 1 + .../backend/src/core/NoteUpdateService.ts | 326 ++++++++++++++++++ .../src/core/activitypub/ApInboxService.ts | 64 +++- .../src/core/activitypub/ApRendererService.ts | 2 + .../core/activitypub/models/ApNoteService.ts | 113 +++++- packages/backend/src/core/activitypub/type.ts | 1 + .../src/server/api/endpoints/notes/update.ts | 125 ++++++- 10 files changed, 627 insertions(+), 25 deletions(-) create mode 100644 packages/backend/migration/1696604572677-poll_vote_poll.js create mode 100644 packages/backend/src/core/NoteUpdateService.ts diff --git a/packages/backend/migration/1696604572677-poll_vote_poll.js b/packages/backend/migration/1696604572677-poll_vote_poll.js new file mode 100644 index 0000000000..da52904565 --- /dev/null +++ b/packages/backend/migration/1696604572677-poll_vote_poll.js @@ -0,0 +1,12 @@ +export class PollVotePoll1696604572677 { + name = 'PollVotePoll1696604572677'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "poll_vote" ADD CONSTRAINT "FK_poll_vote_poll" FOREIGN KEY ("noteId") REFERENCES "poll"("noteId") ON DELETE CASCADE`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "poll_vote" DROP CONSTRAINT "FK_poll_vote_poll"`); + } + +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 33b1726afe..78f5cb3f36 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -32,6 +32,7 @@ import { MetaService } from './MetaService.js'; import { MfmService } from './MfmService.js'; import { ModerationLogService } from './ModerationLogService.js'; import { NoteCreateService } from './NoteCreateService.js'; +import { NoteUpdateService } from './NoteUpdateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; import { NoteReadService } from './NoteReadService.js'; @@ -164,6 +165,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService }; const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService }; +const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; @@ -300,6 +302,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv MfmService, ModerationLogService, NoteCreateService, + NoteUpdateService, NoteDeleteService, NotePiningService, NoteReadService, @@ -429,6 +432,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $MfmService, $ModerationLogService, $NoteCreateService, + $NoteUpdateService, $NoteDeleteService, $NotePiningService, $NoteReadService, @@ -559,6 +563,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv MfmService, ModerationLogService, NoteCreateService, + NoteUpdateService, NoteDeleteService, NotePiningService, NoteReadService, @@ -687,6 +692,7 @@ const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEv $MfmService, $ModerationLogService, $NoteCreateService, + $NoteUpdateService, $NoteDeleteService, $NotePiningService, $NoteReadService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index e00e2962a0..cd83f967d9 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -115,7 +115,7 @@ export interface NoteEventTypes { }; updated: { cw: string | null; - text: string; + text: string | null; }; reacted: { reaction: string; diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 9a4f38a702..c1e50a8a4f 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -125,6 +125,7 @@ type MinimumUser = { type Option = { createdAt?: Date | null; + updatedAt?: Date | null; name?: string | null; text?: string | null; reply?: MiNote | null; diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts new file mode 100644 index 0000000000..8c5964ca65 --- /dev/null +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -0,0 +1,326 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setImmediate } from 'node:timers/promises'; +import { In, DataSource } from 'typeorm'; +import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; +import type { IMentionedRemoteUsers } from '@/models/Note.js'; +import { MiNote } from '@/models/Note.js'; +import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import { RelayService } from '@/core/RelayService.js'; +import { DI } from '@/di-symbols.js'; +import ActiveUsersChart from '@/core/chart/charts/active-users.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; +import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; +import { bindThis } from '@/decorators.js'; +import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { SearchService } from '@/core/SearchService.js'; +import { normalizeForSearch } from "@/misc/normalize-for-search.js"; +import { MiDriveFile } from '@/models/_.js'; +import { MiPoll, IPoll } from '@/models/Poll.js'; +import { MiEvent, IEvent } from '@/models/Event.js'; +import * as mfm from "cherrypick-mfm-js"; +import { concat } from "@/misc/prelude/array.js"; +import { extractHashtags } from "@/misc/extract-hashtags.js"; +import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; +import util from 'util'; + +type MinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + +type Option = { + updatedAt?: Date | null; + files?: MiDriveFile[] | null; + name?: string | null; + text?: string | null; + cw?: string | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + poll?: IPoll | null; + event?: IEvent | null; +}; + +@Injectable() +export class NoteUpdateService implements OnApplicationShutdown { + #shutdownController = new AbortController(); + + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private relayService: RelayService, + private apDeliverManagerService: ApDeliverManagerService, + private apRendererService: ApRendererService, + private searchService: SearchService, + private activeUsersChart: ActiveUsersChart, + ) { } + + @bindThis + public async update(user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + createdAt: MiUser['createdAt']; + isBot: MiUser['isBot']; + }, data: Option, note: MiNote, silent = false): Promise { + if (data.updatedAt == null) data.updatedAt = new Date(); + + if (data.text) { + if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { + data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + } + data.text = data.text.trim(); + } else { + data.text = null; + } + + let tags = data.apHashtags; + let emojis = data.apEmojis; + + // Parse MFM if needed + if (!tags || !emojis) { + const tokens = data.text ? mfm.parse(data.text)! : []; + const cwTokens = data.cw ? mfm.parse(data.cw)! : []; + const choiceTokens = data.poll && data.poll.choices + ? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) + : []; + + const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); + + tags = data.apHashtags ?? extractHashtags(combinedTokens); + + emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens); + } + + tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); + + const updatedNote = await this.updateNote(user, note, data, tags, emojis).then(updatedNote => updatedNote!) + + if (updatedNote) { + setImmediate('post updated', { signal: this.#shutdownController.signal }).then( + () => this.postNoteUpdated(updatedNote, user, silent), + () => { /* aborted, ignore this */ }, + ); + } + + return updatedNote; + } + + @bindThis + private async updateNote(user: { id: MiUser['id']; host: MiUser['host']; }, note: MiNote, data: Option, tags: string[], emojis: string[]) { + const values = new MiNote({ + updatedAt: data.updatedAt!, + fileIds: data.files ? data.files.map(file => file.id) : [], + text: data.text, + hasPoll: data.poll != null, + hasEvent: data.event != null, + cw: data.cw ?? null, + tags: tags.map(tag => normalizeForSearch(tag)), + emojis, + attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + }); + + // 投稿を更新 + try { + if ((note.hasPoll || note.hasEvent) && (values.hasPoll || values.hasEvent)) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.update(MiNote, { id: note.id }, values); + + if (values.hasPoll) { + const old_poll = await transactionalEntityManager.findOneBy(MiPoll, { noteId: note.id }); + if (old_poll!.choices.toString() !== data.poll!.choices.toString() || old_poll!.multiple !== data.poll!.multiple) { + await transactionalEntityManager.delete(MiPoll, { noteId: note.id }); + const poll = new MiPoll({ + noteId: note.id, + choices: data.poll!.choices, + expiresAt: data.poll!.expiresAt, + multiple: data.poll!.multiple, + votes: new Array(data.poll!.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host, + }); + await transactionalEntityManager.insert(MiPoll, poll); + } + } + + if (values.hasEvent) { + const event = new MiEvent({ + start: data.event!.start, + end: data.event!.end ?? undefined, + title: data.event!.title, + metadata: data.event!.metadata, + }); + + await transactionalEntityManager.update(MiEvent, { noteId: note.id }, event); + } + }); + } else if ((!note.hasPoll || !note.hasEvent) && (values.hasPoll || values.hasEvent)) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.update(MiNote, { id: note.id }, values); + + if (values.hasPoll) { + const poll = new MiPoll({ + noteId: note.id, + choices: data.poll!.choices, + expiresAt: data.poll!.expiresAt, + multiple: data.poll!.multiple, + votes: new Array(data.poll!.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host, + }); + + await transactionalEntityManager.insert(MiPoll, poll); + } + + if (values.hasEvent) { + const event = new MiEvent({ + noteId: note.id, + start: data.event!.start, + end: data.event!.end ?? undefined, + title: data.event!.title, + metadata: data.event!.metadata, + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host, + }); + + await transactionalEntityManager.insert(MiEvent, event); + } + }); + } else if ((note.hasPoll || note.hasEvent) && (!values.hasPoll || !values.hasEvent)) { + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + await transactionalEntityManager.update(MiNote, {id: note.id}, values); + + if (!values.hasPoll) { + await transactionalEntityManager.delete(MiPoll, {noteId: note.id}); + } + + if (!values.hasEvent) { + await transactionalEntityManager.delete(MiEvent, {noteId: note.id}); + } + }); + } else { + await this.notesRepository.update({ id: note.id }, values); + } + + return await this.notesRepository.findOneBy({ id: note.id }); + } catch (e) { + console.error(e); + + throw e; + } + } + + @bindThis + private async postNoteUpdated(note: MiNote, user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + createdAt: MiUser['createdAt']; + isBot: MiUser['isBot']; + }, silent: boolean) { + if (!silent) { + if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); + + this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, text: note.text }); + + //#region AP deliver + if (this.userEntityService.isLocalUser(user)) { + await (async () => { + // @ts-ignore + const noteActivity = await this.renderNoteActivity(note, user); + + await this.deliverToConcerned(user, note, noteActivity); + })(); + } + //#endregion + } + + // Register to search database + this.reIndex(note); + } + + @bindThis + private async renderNoteActivity(note: MiNote, user: MiUser) { + const content = this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user); + + return this.apRendererService.addContext(content); + } + + @bindThis + private async getMentionedRemoteUsers(note: MiNote) { + const where = [] as any[]; + + // mention / reply / dm + const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); + if (uris.length > 0) { + where.push( + { uri: In(uris) }, + ); + } + + // renote / quote + if (note.renoteUserId) { + where.push({ + id: note.renoteUserId, + }); + } + + if (where.length === 0) return []; + + return await this.usersRepository.find({ + where, + }) as MiRemoteUser[]; + } + + @bindThis + private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { + console.log('deliverToConcerned', util.inspect(content, { depth: null })); + await this.apDeliverManagerService.deliverToFollowers(user, content); + await this.relayService.deliverToRelays(user, content); + const remoteUsers = await this.getMentionedRemoteUsers(note); + for (const remoteUser of remoteUsers) { + await this.apDeliverManagerService.deliverToUser(user, content, remoteUser); + } + } + + @bindThis + private reIndex(note: MiNote) { + if (note.text == null && note.cw == null) return; + + this.searchService.unindexNote(note); + this.searchService.indexNote(note); + } + + @bindThis + public dispose(): void { + this.#shutdownController.abort(); + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index cc82e9000f..d9845e4a67 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; import { NoteDeleteService } from '@/core/NoteDeleteService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; @@ -78,6 +79,7 @@ export class ApInboxService { private notePiningService: NotePiningService, private userBlockingService: UserBlockingService, private noteCreateService: NoteCreateService, + private noteUpdateService: NoteUpdateService, private noteDeleteService: NoteDeleteService, private appLockService: AppLockService, private apResolverService: ApResolverService, @@ -775,30 +777,88 @@ export class ApInboxService { @bindThis private async update(actor: MiRemoteUser, activity: IUpdate): Promise { + const uri = getApId(activity); + if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } - this.logger.debug('Update'); + this.logger.debug(`Update: ${uri}`); const resolver = this.apResolverService.createResolver(); + //@ts-ignore + for (const key in activity.object) { + // @ts-ignore + this.logger.info(`Update Inbox: ${key}: ${activity.object[key]}`); + } + + const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); throw e; }); + this.logger.info('Update Inbox: 1'); + if (isActor(object)) { + this.logger.info('Update Inbox: 2'); await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; - } else if (getApType(object) === 'Question') { + } /*else if (getApType(object) === 'Question') { + this.logger.info('Update Inbox: 3'); await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); return 'ok: Question updated'; + }*/ else if (getApType(object) === 'Note' || getApType(object) === 'Question') { + this.logger.info('Update Inbox: 4'); + await this.updateNote(resolver, actor, object, false, activity); + return 'ok: Note updated'; } else { return `skip: Unknown type: ${getApType(object)}`; } } + @bindThis + private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise { + const uri = getApId(note); + + this.logger.info('Update Note Inbox... 1'); + + if (typeof note === 'object') { + if (actor.uri !== note.attributedTo) { + return 'skip: actor.uri !== note.attributedTo'; + } + + if (typeof note.id === 'string') { + if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) { + return 'skip: host in actor.uri !== note.id'; + } + } + } + + this.logger.info('Update Note Inbox... 2'); + + const unlock = await this.appLockService.getApLock(uri); + + try { + //const exist = await this.apNoteService.fetchNote(note); + //if (exist) return 'skip: note exists'; + + this.logger.info('Update Note Inbox... 3'); + + await this.apNoteService.updateNote(note, resolver, silent); + return 'ok'; + } catch (err) { + if (err instanceof StatusError && err.isClientError) { + return `skip ${err.statusCode}`; + } else { + throw err; + } + } finally { + unlock(); + } + } + @bindThis private async move(actor: MiRemoteUser, activity: IMove): Promise { // fetch the new and old accounts diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index b0504bdef0..82bf790e99 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -110,6 +110,7 @@ export class ApRendererService { actor: this.userEntityService.genLocalUserUri(note.userId), type: 'Announce', published: note.createdAt.toISOString(), + updated: note.updatedAt?.toISOString() ?? undefined, to, cc, object, @@ -458,6 +459,7 @@ export class ApRendererService { _misskey_quote: quote, quoteUrl: quote, published: note.createdAt.toISOString(), + updated: note.updatedAt?.toISOString() ?? undefined, to, cc, inReplyTo, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index e4480a30ba..f72b428c2a 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -7,7 +7,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/_.js'; +import type { EmojisRepository, MessagingMessagesRepository, NotesRepository, PollsRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -25,10 +25,12 @@ import { UtilityService } from '@/core/UtilityService.js'; import { MessagingService } from '@/core/MessagingService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; -import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; +import type { IObject, IPost } from '../type.js'; +import { getApId, getApType, getOneApHrefNullable, getOneApId, isEmoji, validPost } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; import { ApDbResolverService } from '../ApDbResolverService.js'; +import type { Resolver } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApAudienceService } from '../ApAudienceService.js'; import { ApPersonService } from './ApPersonService.js'; @@ -37,8 +39,7 @@ import { ApMentionService } from './ApMentionService.js'; import { ApQuestionService } from './ApQuestionService.js'; import { ApEventService } from './ApEventService.js'; import { ApImageService } from './ApImageService.js'; -import type { Resolver } from '../ApResolverService.js'; -import type { IObject, IPost } from '../type.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; @Injectable() export class ApNoteService { @@ -57,6 +58,9 @@ export class ApNoteService { @Inject(DI.messagingMessagesRepository) private messagingMessagesRepository: MessagingMessagesRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + private idService: IdService, private apMfmService: ApMfmService, private apResolverService: ApResolverService, @@ -76,6 +80,7 @@ export class ApNoteService { private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, + private noteUpdateService: NoteUpdateService, private apDbResolverService: ApDbResolverService, private apLoggerService: ApLoggerService, ) { @@ -302,6 +307,7 @@ export class ApNoteService { try { return await this.noteCreateService.create(actor, { createdAt: note.published ? new Date(note.published) : null, + updatedAt: note.updated ? new Date(note.updated) : null, files, reply, renote: quote, @@ -333,6 +339,105 @@ export class ApNoteService { } } + @bindThis + public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + if (resolver == null) resolver = this.apResolverService.createResolver(); + + const object = await resolver.resolve(value); + const entryUri = getApId(value); + + const err = this.validateNote(object, entryUri); + if (err) { + this.logger.error(err.message, { + resolver: { history: resolver.getHistory() }, + value, + object, + }); + throw new Error('invalid note'); + } + + this.logger.info('Update Note Process... 1'); + + const note = object as IPost; + + // 投稿者をフェッチ + if (note.attributedTo == null) { + throw new Error('invalid note.attributedTo: ' + note.attributedTo); + } + + const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser; + + // 投稿者が凍結されていたらスキップ + if (actor.isSuspended) { + throw new Error('actor has been suspended'); + } + + const b_note = await this.notesRepository.findOneBy({ + uri: entryUri + }).then(x => { + if (x == null) throw new Error('note not found'); + return x; + }); + + this.logger.info('Update Note Process... 2'); + + const limit = promiseLimit(2); + const files = (await Promise.all(toArray(note.attachment).map(attach => ( + limit(() => this.apImageService.resolveImage(actor, { + ...attach, + sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + })) + )))); + + const cw = note.summary === '' ? null : note.summary; + + this.logger.info('Update Note Process... 3'); + + // テキストのパース + let text: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { + text = note.source.content; + } else if (typeof note._misskey_content !== 'undefined') { + text = note._misskey_content; + } else if (typeof note.content === 'string') { + text = this.apMfmService.htmlToMfm(note.content, note.tag); + } + + const apHashtags = extractApHashtags(note.tag); + + const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { + this.logger.info(`extractEmojis: ${e}`); + return []; + }); + + const apEmojis = emojis.map(emoji => emoji.name); + + this.logger.info('Update Note Process... 4'); + + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); + const event = await this.apEventService.extractEventFromNote(note, resolver).catch(() => undefined); + + this.logger.info('Update Note Process... 5'); + this.logger.info(`updateNote: ${note.id}, ${note.updated}, ${note.name}, ${cw}, ${text}, ${apHashtags}, ${apEmojis}, ${poll}, ${event}`); + + try { + this.logger.info('Update Note Process... 6'); + return await this.noteUpdateService.update(actor, { + updatedAt: note.updated ? new Date(note.updated) : null, + files, + name: note.name, + cw, + text, + apHashtags, + apEmojis, + poll, + event, + }, b_note, silent); + } catch (err: any) { + this.logger.info('note update error'); + } + } + /** * Noteを解決します。 * diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index eda7bfc142..b7e8f4b398 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -13,6 +13,7 @@ export interface IObject { name?: string | null; summary?: string; published?: string; + updated?: string; cc?: ApObject; to?: ApObject; attributedTo?: ApObject; diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index 311a0aafd2..1ead458212 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -5,14 +5,14 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteUpdateService } from '@/core/NoteUpdateService.js'; import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { ApiError } from '../../error.js'; +import type { DriveFilesRepository, MiDriveFile } from "@/models/_.js"; export const meta = { tags: ['notes'], @@ -34,6 +34,16 @@ export const meta = { code: 'NO_SUCH_NOTE', id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', }, + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, + cannotCreateAlreadyExpiredPoll: { + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', + }, }, } as const; @@ -47,7 +57,49 @@ export const paramDef = { maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false, }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, + event: { + type: 'object', + nullable: true, + properties: { + title: {type: 'string', minLength: 1, maxLength: 128, nullable: false}, + start: {type: 'integer', nullable: false}, + end: {type: 'integer', nullable: true}, + metadata: {type: 'object'}, + }, + }, cw: { type: 'string', nullable: true, maxLength: 100 }, + disableRightClick: { type: 'boolean', default: false }, }, required: ['noteId', 'text', 'cw'], } as const; @@ -55,14 +107,12 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, private getterService: GetterService, - private globalEventService: GlobalEventService, + private noteEntityService: NoteEntityService, + private noteUpdateService: NoteUpdateService, ) { super(meta, paramDef, async (ps, me) => { const note = await this.getterService.getNote(ps.noteId).catch(err => { @@ -74,17 +124,56 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchNote); } - await this.notesRepository.update({ id: note.id }, { - updatedAt: new Date(), - cw: ps.cw, - text: ps.text, - noteEditHistory: [...note.noteEditHistory, note.text!], - }); - this.globalEventService.publishNoteStream(note.id, 'updated', { - cw: ps.cw, + let files: MiDriveFile[] = []; + const fileIds = ps.fileIds ?? ps.mediaIds ?? null; + if (fileIds != null) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + + if (files.length !== fileIds.length) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + if (ps.poll) { + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; + } + } + + const data = { text: ps.text, - }); + files: files, + cw: ps.cw, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + event: ps.event ? { + start: new Date(ps.event.start!), + end: ps.event.end ? new Date(ps.event.end) : null, + title: ps.event.title!, + metadata: ps.event.metadata ?? {}, + } : undefined, + }; + + const updatedNote = await this.noteUpdateService.update(me, data, note, false); + + return { + updatedNote: await this.noteEntityService.pack(updatedNote, me), + }; }); } } From 36ed8a17c313cce02ac07d05ca7790a491d0588d Mon Sep 17 00:00:00 2001 From: caipira113 Date: Fri, 20 Oct 2023 02:09:38 +0900 Subject: [PATCH 2/5] 2 --- packages/backend/src/core/NoteUpdateService.ts | 8 +++++--- .../backend/src/core/activitypub/models/ApNoteService.ts | 3 ++- packages/backend/src/server/api/endpoints/notes/update.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index 35e8ae3569..0adf88b4d0 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -78,7 +78,7 @@ export class NoteUpdateService implements OnApplicationShutdown { username: MiUser['username']; host: MiUser['host']; isBot: MiUser['isBot']; - }, data: Option, note: MiNote, silent = false): Promise { + }, data: Option, note: MiNote, silent = false): Promise { if (data.updatedAt == null) data.updatedAt = new Date(); if (data.text) { @@ -110,7 +110,7 @@ export class NoteUpdateService implements OnApplicationShutdown { tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32); - const updatedNote = await this.updateNote(user, note, data, tags, emojis).then(updatedNote => updatedNote!) + const updatedNote = await this.updateNote(user, note, data, tags, emojis); if (updatedNote) { setImmediate('post updated', { signal: this.#shutdownController.signal }).then( @@ -123,7 +123,9 @@ export class NoteUpdateService implements OnApplicationShutdown { } @bindThis - private async updateNote(user: { id: MiUser['id']; host: MiUser['host']; }, note: MiNote, data: Option, tags: string[], emojis: string[]) { + private async updateNote(user: { + id: MiUser['id']; host: MiUser['host']; + }, note: MiNote, data: Option, tags: string[], emojis: string[]): Promise { const updatedAtHistory = note.updatedAtHistory ? note.updatedAtHistory : []; const values = new MiNote({ diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index e5c76a0179..0b91958f1b 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -340,7 +340,7 @@ export class ApNoteService { } @bindThis - public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { + public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); const object = await resolver.resolve(value); @@ -435,6 +435,7 @@ export class ApNoteService { }, b_note, silent); } catch (err: any) { this.logger.info('note update error'); + return err; } } diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index 1ead458212..4f4bbf5b32 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -172,7 +172,7 @@ export default class extends Endpoint { // eslint- const updatedNote = await this.noteUpdateService.update(me, data, note, false); return { - updatedNote: await this.noteEntityService.pack(updatedNote, me), + updatedNote: await this.noteEntityService.pack(updatedNote!, me), }; }); } From 1fc5453d855e1c4338e8d34a4848f74400ca3495 Mon Sep 17 00:00:00 2001 From: caipira113 Date: Fri, 20 Oct 2023 23:54:36 +0900 Subject: [PATCH 3/5] fix --- .../backend/src/core/NoteUpdateService.ts | 39 ++----------------- .../core/activitypub/models/ApNoteService.ts | 2 - .../src/server/api/endpoints/notes/update.ts | 16 -------- 3 files changed, 3 insertions(+), 54 deletions(-) diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index 0adf88b4d0..d52c744b25 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -23,7 +23,6 @@ import { SearchService } from '@/core/SearchService.js'; import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import { MiDriveFile } from '@/models/_.js'; import { MiPoll, IPoll } from '@/models/Poll.js'; -import { MiEvent, IEvent } from '@/models/Event.js'; import * as mfm from "cherrypick-mfm-js"; import { concat } from "@/misc/prelude/array.js"; import { extractHashtags } from "@/misc/extract-hashtags.js"; @@ -46,7 +45,6 @@ type Option = { apHashtags?: string[] | null; apEmojis?: string[] | null; poll?: IPoll | null; - event?: IEvent | null; }; @Injectable() @@ -133,7 +131,6 @@ export class NoteUpdateService implements OnApplicationShutdown { fileIds: data.files ? data.files.map(file => file.id) : [], text: data.text, hasPoll: data.poll != null, - hasEvent: data.event != null, cw: data.cw ?? null, tags: tags.map(tag => normalizeForSearch(tag)), emojis, @@ -144,7 +141,7 @@ export class NoteUpdateService implements OnApplicationShutdown { // 投稿を更新 try { - if ((note.hasPoll || note.hasEvent) && (values.hasPoll || values.hasEvent)) { + if (note.hasPoll && values.hasPoll) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, { id: note.id }, values); @@ -166,19 +163,8 @@ export class NoteUpdateService implements OnApplicationShutdown { await transactionalEntityManager.insert(MiPoll, poll); } } - - if (values.hasEvent) { - const event = new MiEvent({ - start: data.event!.start, - end: data.event!.end ?? undefined, - title: data.event!.title, - metadata: data.event!.metadata, - }); - - await transactionalEntityManager.update(MiEvent, { noteId: note.id }, event); - } }); - } else if ((!note.hasPoll || !note.hasEvent) && (values.hasPoll || values.hasEvent)) { + } else if (!note.hasPoll && values.hasPoll) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, { id: note.id }, values); @@ -197,23 +183,8 @@ export class NoteUpdateService implements OnApplicationShutdown { await transactionalEntityManager.insert(MiPoll, poll); } - - if (values.hasEvent) { - const event = new MiEvent({ - noteId: note.id, - start: data.event!.start, - end: data.event!.end ?? undefined, - title: data.event!.title, - metadata: data.event!.metadata, - noteVisibility: note.visibility, - userId: user.id, - userHost: user.host, - }); - - await transactionalEntityManager.insert(MiEvent, event); - } }); - } else if ((note.hasPoll || note.hasEvent) && (!values.hasPoll || !values.hasEvent)) { + } else if (note.hasPoll && !values.hasPoll) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, {id: note.id}, values); @@ -221,10 +192,6 @@ export class NoteUpdateService implements OnApplicationShutdown { if (!values.hasPoll) { await transactionalEntityManager.delete(MiPoll, {noteId: note.id}); } - - if (!values.hasEvent) { - await transactionalEntityManager.delete(MiEvent, {noteId: note.id}); - } }); } else { await this.notesRepository.update({ id: note.id }, values); diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2e3d64cc81..ed76743e6a 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -410,7 +410,6 @@ export class ApNoteService { this.logger.info('Update Note Process... 4'); const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - const event = await this.apEventService.extractEventFromNote(note, resolver).catch(() => undefined); this.logger.info('Update Note Process... 5'); this.logger.info(`updateNote: ${note.id}, ${note.updated}, ${note.name}, ${cw}, ${text}, ${apHashtags}, ${apEmojis}, ${poll}, ${event}`); @@ -426,7 +425,6 @@ export class ApNoteService { apHashtags, apEmojis, poll, - event, }, b_note, silent); } catch (err: any) { this.logger.info('note update error'); diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts index 4f4bbf5b32..f5c5e50ae5 100644 --- a/packages/backend/src/server/api/endpoints/notes/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -88,16 +88,6 @@ export const paramDef = { }, required: ['choices'], }, - event: { - type: 'object', - nullable: true, - properties: { - title: {type: 'string', minLength: 1, maxLength: 128, nullable: false}, - start: {type: 'integer', nullable: false}, - end: {type: 'integer', nullable: true}, - metadata: {type: 'object'}, - }, - }, cw: { type: 'string', nullable: true, maxLength: 100 }, disableRightClick: { type: 'boolean', default: false }, }, @@ -161,12 +151,6 @@ export default class extends Endpoint { // eslint- multiple: ps.poll.multiple ?? false, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, } : undefined, - event: ps.event ? { - start: new Date(ps.event.start!), - end: ps.event.end ? new Date(ps.event.end) : null, - title: ps.event.title!, - metadata: ps.event.metadata ?? {}, - } : undefined, }; const updatedNote = await this.noteUpdateService.update(me, data, note, false); From ee4c3727041f3a34cbd2b32f8c0df2dde437fc0d Mon Sep 17 00:00:00 2001 From: caipira113 Date: Sat, 21 Oct 2023 00:19:18 +0900 Subject: [PATCH 4/5] asdf --- .../backend/src/core/activitypub/ApInboxService.ts | 12 ------------ .../src/core/activitypub/models/ApNoteService.ts | 14 +------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index c774c2a483..c29ff0d613 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -798,18 +798,13 @@ export class ApInboxService { throw e; }); - this.logger.info('Update Inbox: 1'); - if (isActor(object)) { - this.logger.info('Update Inbox: 2'); await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } /*else if (getApType(object) === 'Question') { - this.logger.info('Update Inbox: 3'); await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); return 'ok: Question updated'; }*/ else if (getApType(object) === 'Note' || getApType(object) === 'Question') { - this.logger.info('Update Inbox: 4'); await this.updateNote(resolver, actor, object, false, activity); return 'ok: Note updated'; } else { @@ -821,8 +816,6 @@ export class ApInboxService { private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise { const uri = getApId(note); - this.logger.info('Update Note Inbox... 1'); - if (typeof note === 'object') { if (actor.uri !== note.attributedTo) { return 'skip: actor.uri !== note.attributedTo'; @@ -835,16 +828,11 @@ export class ApInboxService { } } - this.logger.info('Update Note Inbox... 2'); - const unlock = await this.appLockService.getApLock(uri); try { //const exist = await this.apNoteService.fetchNote(note); //if (exist) return 'skip: note exists'; - - this.logger.info('Update Note Inbox... 3'); - await this.apNoteService.updateNote(note, resolver, silent); return 'ok'; } catch (err) { diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index ed76743e6a..2d535ce814 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -351,8 +351,6 @@ export class ApNoteService { throw new Error('invalid note'); } - this.logger.info('Update Note Process... 1'); - const note = object as IPost; // 投稿者をフェッチ @@ -374,8 +372,6 @@ export class ApNoteService { return x; }); - this.logger.info('Update Note Process... 2'); - const limit = promiseLimit(2); const files = (await Promise.all(toArray(note.attachment).map(attach => ( limit(() => this.apImageService.resolveImage(actor, { @@ -386,8 +382,6 @@ export class ApNoteService { const cw = note.summary === '' ? null : note.summary; - this.logger.info('Update Note Process... 3'); - // テキストのパース let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { @@ -407,15 +401,9 @@ export class ApNoteService { const apEmojis = emojis.map(emoji => emoji.name); - this.logger.info('Update Note Process... 4'); - const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - this.logger.info('Update Note Process... 5'); - this.logger.info(`updateNote: ${note.id}, ${note.updated}, ${note.name}, ${cw}, ${text}, ${apHashtags}, ${apEmojis}, ${poll}, ${event}`); - try { - this.logger.info('Update Note Process... 6'); return await this.noteUpdateService.update(actor, { updatedAt: note.updated ? new Date(note.updated) : null, files, @@ -427,7 +415,7 @@ export class ApNoteService { poll, }, b_note, silent); } catch (err: any) { - this.logger.info('note update error'); + this.logger.warn(`note update failed: ${err}`); return err; } } From db395bbf2a3bcde162ab916cddff3a5c94e2b311 Mon Sep 17 00:00:00 2001 From: caipira113 Date: Sat, 21 Oct 2023 00:20:50 +0900 Subject: [PATCH 5/5] asdf --- packages/backend/src/core/activitypub/ApInboxService.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index c29ff0d613..8e039641ba 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -786,13 +786,6 @@ export class ApInboxService { const resolver = this.apResolverService.createResolver(); - //@ts-ignore - for (const key in activity.object) { - // @ts-ignore - this.logger.info(`Update Inbox: ${key}: ${activity.object[key]}`); - } - - const object = await resolver.resolve(activity.object).catch(e => { this.logger.error(`Resolution failed: ${e}`); throw e;