feat(backend): Federated note update (#1)
(cherry picked from commit 6af23d4e28893b0ab253182153973bcad1210ac0)
This commit is contained in:
parent
5812b15cbd
commit
e9fda7dd1a
|
@ -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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ import { MetaService } from './MetaService.js';
|
||||||
import { MfmService } from './MfmService.js';
|
import { MfmService } from './MfmService.js';
|
||||||
import { ModerationLogService } from './ModerationLogService.js';
|
import { ModerationLogService } from './ModerationLogService.js';
|
||||||
import { NoteCreateService } from './NoteCreateService.js';
|
import { NoteCreateService } from './NoteCreateService.js';
|
||||||
|
import { NoteUpdateService } from './NoteUpdateService.js';
|
||||||
import { NoteDeleteService } from './NoteDeleteService.js';
|
import { NoteDeleteService } from './NoteDeleteService.js';
|
||||||
import { NotePiningService } from './NotePiningService.js';
|
import { NotePiningService } from './NotePiningService.js';
|
||||||
import { NoteReadService } from './NoteReadService.js';
|
import { NoteReadService } from './NoteReadService.js';
|
||||||
|
@ -171,6 +172,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic
|
||||||
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
|
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
|
||||||
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
|
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
|
||||||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
||||||
|
const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService };
|
||||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
||||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
||||||
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
||||||
|
@ -311,6 +313,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
MfmService,
|
MfmService,
|
||||||
ModerationLogService,
|
ModerationLogService,
|
||||||
NoteCreateService,
|
NoteCreateService,
|
||||||
|
NoteUpdateService,
|
||||||
NoteDeleteService,
|
NoteDeleteService,
|
||||||
NotePiningService,
|
NotePiningService,
|
||||||
NoteReadService,
|
NoteReadService,
|
||||||
|
@ -447,6 +450,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$MfmService,
|
$MfmService,
|
||||||
$ModerationLogService,
|
$ModerationLogService,
|
||||||
$NoteCreateService,
|
$NoteCreateService,
|
||||||
|
$NoteUpdateService,
|
||||||
$NoteDeleteService,
|
$NoteDeleteService,
|
||||||
$NotePiningService,
|
$NotePiningService,
|
||||||
$NoteReadService,
|
$NoteReadService,
|
||||||
|
@ -584,6 +588,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
MfmService,
|
MfmService,
|
||||||
ModerationLogService,
|
ModerationLogService,
|
||||||
NoteCreateService,
|
NoteCreateService,
|
||||||
|
NoteUpdateService,
|
||||||
NoteDeleteService,
|
NoteDeleteService,
|
||||||
NotePiningService,
|
NotePiningService,
|
||||||
NoteReadService,
|
NoteReadService,
|
||||||
|
@ -719,6 +724,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$MfmService,
|
$MfmService,
|
||||||
$ModerationLogService,
|
$ModerationLogService,
|
||||||
$NoteCreateService,
|
$NoteCreateService,
|
||||||
|
$NoteUpdateService,
|
||||||
$NoteDeleteService,
|
$NoteDeleteService,
|
||||||
$NotePiningService,
|
$NotePiningService,
|
||||||
$NoteReadService,
|
$NoteReadService,
|
||||||
|
|
|
@ -117,7 +117,7 @@ export interface NoteEventTypes {
|
||||||
};
|
};
|
||||||
updated: {
|
updated: {
|
||||||
cw: string | null;
|
cw: string | null;
|
||||||
text: string;
|
text: string | null;
|
||||||
};
|
};
|
||||||
reacted: {
|
reacted: {
|
||||||
reaction: string;
|
reaction: string;
|
||||||
|
|
|
@ -115,6 +115,38 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MinimumUser = {
|
||||||
|
id: MiUser['id'];
|
||||||
|
host: MiUser['host'];
|
||||||
|
username: MiUser['username'];
|
||||||
|
uri: MiUser['uri'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
createdAt?: Date | null;
|
||||||
|
updatedAt?: Date | null;
|
||||||
|
name?: string | null;
|
||||||
|
text?: string | null;
|
||||||
|
reply?: MiNote | null;
|
||||||
|
renote?: MiNote | null;
|
||||||
|
files?: MiDriveFile[] | null;
|
||||||
|
poll?: IPoll | null;
|
||||||
|
event?: IEvent | null;
|
||||||
|
localOnly?: boolean | null;
|
||||||
|
reactionAcceptance?: MiNote['reactionAcceptance'];
|
||||||
|
disableRightClick?: boolean | null;
|
||||||
|
cw?: string | null;
|
||||||
|
visibility?: string;
|
||||||
|
visibleUsers?: MinimumUser[] | null;
|
||||||
|
channel?: MiChannel | null;
|
||||||
|
apMentions?: MinimumUser[] | null;
|
||||||
|
apHashtags?: string[] | null;
|
||||||
|
apEmojis?: string[] | null;
|
||||||
|
uri?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
app?: MiApp | null;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
|
@ -0,0 +1,297 @@
|
||||||
|
/*
|
||||||
|
* 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 * 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
@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'];
|
||||||
|
isBot: MiUser['isBot'];
|
||||||
|
}, data: Option, note: MiNote, silent = false): Promise<MiNote | null> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
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[]): Promise<MiNote | null> {
|
||||||
|
const updatedAtHistory = note.updatedAtHistory ? note.updatedAtHistory : [];
|
||||||
|
|
||||||
|
const values = new MiNote({
|
||||||
|
updatedAt: data.updatedAt!,
|
||||||
|
fileIds: data.files ? data.files.map(file => file.id) : [],
|
||||||
|
text: data.text,
|
||||||
|
hasPoll: data.poll != null,
|
||||||
|
cw: data.cw ?? null,
|
||||||
|
tags: tags.map(tag => normalizeForSearch(tag)),
|
||||||
|
emojis,
|
||||||
|
attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
|
||||||
|
updatedAtHistory: [...updatedAtHistory, new Date()],
|
||||||
|
noteEditHistory: [...note.noteEditHistory, (note.cw ? note.cw + '\n' : '') + note.text!],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 投稿を更新
|
||||||
|
try {
|
||||||
|
if (note.hasPoll && values.hasPoll) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!note.hasPoll && values.hasPoll) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (note.hasPoll && !values.hasPoll) {
|
||||||
|
// 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});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} 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'];
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||||
|
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
|
||||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||||
import { AppLockService } from '@/core/AppLockService.js';
|
import { AppLockService } from '@/core/AppLockService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
@ -73,6 +74,7 @@ export class ApInboxService {
|
||||||
private notePiningService: NotePiningService,
|
private notePiningService: NotePiningService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
private noteUpdateService: NoteUpdateService,
|
||||||
private noteDeleteService: NoteDeleteService,
|
private noteDeleteService: NoteDeleteService,
|
||||||
private appLockService: AppLockService,
|
private appLockService: AppLockService,
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
|
@ -730,11 +732,13 @@ export class ApInboxService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
||||||
|
const uri = getApId(activity);
|
||||||
|
|
||||||
if (actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Update');
|
this.logger.debug(`Update: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
|
@ -746,14 +750,51 @@ export class ApInboxService {
|
||||||
if (isActor(object)) {
|
if (isActor(object)) {
|
||||||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
} else if (getApType(object) === 'Question') {
|
} /*else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
||||||
return 'ok: Question updated';
|
return 'ok: Question updated';
|
||||||
|
}*/ else if (getApType(object) === 'Note' || getApType(object) === 'Question') {
|
||||||
|
await this.updateNote(resolver, actor, object, false, activity);
|
||||||
|
return 'ok: Note updated';
|
||||||
} else {
|
} else {
|
||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unknown type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise<string> {
|
||||||
|
const uri = getApId(note);
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
//const exist = await this.apNoteService.fetchNote(note);
|
||||||
|
//if (exist) return 'skip: note exists';
|
||||||
|
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
|
@bindThis
|
||||||
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
||||||
// fetch the new and old accounts
|
// fetch the new and old accounts
|
||||||
|
|
|
@ -108,6 +108,7 @@ export class ApRendererService {
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
|
updated: note.updatedAt?.toISOString() ?? undefined,
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
object,
|
object,
|
||||||
|
@ -437,6 +438,7 @@ export class ApRendererService {
|
||||||
_misskey_quote: quote,
|
_misskey_quote: quote,
|
||||||
quoteUrl: quote,
|
quoteUrl: quote,
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
|
updated: note.updatedAt?.toISOString() ?? undefined,
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
inReplyTo,
|
inReplyTo,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
* SPDX-FileCopyrightText: syuilo and misskey-project, cherrypick contributors
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
@ -24,10 +24,12 @@ import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.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 { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApMfmService } from '../ApMfmService.js';
|
import { ApMfmService } from '../ApMfmService.js';
|
||||||
import { ApDbResolverService } from '../ApDbResolverService.js';
|
import { ApDbResolverService } from '../ApDbResolverService.js';
|
||||||
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import { ApAudienceService } from '../ApAudienceService.js';
|
import { ApAudienceService } from '../ApAudienceService.js';
|
||||||
import { ApPersonService } from './ApPersonService.js';
|
import { ApPersonService } from './ApPersonService.js';
|
||||||
|
@ -35,8 +37,7 @@ import { extractApHashtags } from './tag.js';
|
||||||
import { ApMentionService } from './ApMentionService.js';
|
import { ApMentionService } from './ApMentionService.js';
|
||||||
import { ApQuestionService } from './ApQuestionService.js';
|
import { ApQuestionService } from './ApQuestionService.js';
|
||||||
import { ApImageService } from './ApImageService.js';
|
import { ApImageService } from './ApImageService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
|
||||||
import type { IObject, IPost } from '../type.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApNoteService {
|
export class ApNoteService {
|
||||||
|
@ -52,6 +53,12 @@ export class ApNoteService {
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
@Inject(DI.messagingMessagesRepository)
|
||||||
|
private messagingMessagesRepository: MessagingMessagesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private apMfmService: ApMfmService,
|
private apMfmService: ApMfmService,
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
|
@ -69,6 +76,7 @@ export class ApNoteService {
|
||||||
private appLockService: AppLockService,
|
private appLockService: AppLockService,
|
||||||
private pollService: PollService,
|
private pollService: PollService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
private noteUpdateService: NoteUpdateService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private apLoggerService: ApLoggerService,
|
private apLoggerService: ApLoggerService,
|
||||||
) {
|
) {
|
||||||
|
@ -278,6 +286,7 @@ export class ApNoteService {
|
||||||
try {
|
try {
|
||||||
return await this.noteCreateService.create(actor, {
|
return await this.noteCreateService.create(actor, {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
|
updatedAt: note.updated ? new Date(note.updated) : null,
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
renote: quote,
|
renote: quote,
|
||||||
|
@ -307,6 +316,92 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
const limit = promiseLimit<MiDriveFile>(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;
|
||||||
|
|
||||||
|
// テキストのパース
|
||||||
|
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);
|
||||||
|
|
||||||
|
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.noteUpdateService.update(actor, {
|
||||||
|
updatedAt: note.updated ? new Date(note.updated) : null,
|
||||||
|
files,
|
||||||
|
name: note.name,
|
||||||
|
cw,
|
||||||
|
text,
|
||||||
|
apHashtags,
|
||||||
|
apEmojis,
|
||||||
|
poll,
|
||||||
|
}, b_note, silent);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`note update failed: ${err}`);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noteを解決します。
|
* Noteを解決します。
|
||||||
*
|
*
|
||||||
|
|
|
@ -14,6 +14,7 @@ export interface IObject {
|
||||||
summary?: string;
|
summary?: string;
|
||||||
_misskey_summary?: string;
|
_misskey_summary?: string;
|
||||||
published?: string;
|
published?: string;
|
||||||
|
updated?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
attributedTo?: ApObject;
|
attributedTo?: ApObject;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors, cherrypick contributors
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -7,12 +7,13 @@ import ms from 'ms';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, NotesRepository, DriveFilesRepository, MiDriveFile } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, DriveFilesRepository, MiDriveFile } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.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 { DI } from '@/di-symbols.js';
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
import type { DriveFilesRepository, MiDriveFile } from "@/models/_.js";
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -39,6 +40,11 @@ export const meta = {
|
||||||
code: 'NO_SUCH_FILE',
|
code: 'NO_SUCH_FILE',
|
||||||
id: 'b6992544-63e7-67f0-fa7f-32444b1b5306',
|
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;
|
} as const;
|
||||||
|
|
||||||
|
@ -66,7 +72,7 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
maxLength: MAX_NOTE_TEXT_LENGTH,
|
maxLength: MAX_NOTE_TEXT_LENGTH,
|
||||||
nullable: true,
|
nullable: false,
|
||||||
},
|
},
|
||||||
fileIds: {
|
fileIds: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
@ -99,31 +105,23 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
disableRightClick: { type: 'boolean', default: false },
|
||||||
},
|
},
|
||||||
// (re)note with text, files and poll are optional
|
required: ['noteId', 'text', 'cw'],
|
||||||
anyOf: [
|
|
||||||
{ required: ['text'] },
|
|
||||||
{ required: ['renoteId'] },
|
|
||||||
{ required: ['fileIds'] },
|
|
||||||
{ required: ['mediaIds'] },
|
|
||||||
{ required: ['poll'] },
|
|
||||||
],
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private usersRepository: UsersRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
|
||||||
private notesRepository: NotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
private globalEventService: GlobalEventService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
private noteUpdateService: NoteUpdateService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
const note = await this.getterService.getNote(ps.noteId).catch(err => {
|
||||||
|
@ -131,9 +129,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
let files: MiDriveFile[] = [];
|
if (note.userId !== me.id) {
|
||||||
const fileIds = ps.fileIds ?? null;
|
throw new ApiError(meta.errors.noSuchNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let files: MiDriveFile[] = [];
|
||||||
|
const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
|
||||||
if (fileIds != null) {
|
if (fileIds != null) {
|
||||||
files = await this.driveFilesRepository.createQueryBuilder('file')
|
files = await this.driveFilesRepository.createQueryBuilder('file')
|
||||||
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
|
||||||
|
@ -149,31 +151,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.userId !== me.id) {
|
if (ps.poll) {
|
||||||
throw new ApiError(meta.errors.noSuchNote);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.notesRepository.update({ id: note.id }, {
|
const data = {
|
||||||
updatedAt: new Date(),
|
|
||||||
cw: ps.cw,
|
|
||||||
text: ps.text,
|
text: ps.text,
|
||||||
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
|
files: files,
|
||||||
|
cw: ps.cw,
|
||||||
poll: ps.poll ? {
|
poll: ps.poll ? {
|
||||||
choices: ps.poll.choices,
|
choices: ps.poll.choices,
|
||||||
multiple: ps.poll.multiple ?? false,
|
multiple: ps.poll.multiple ?? false,
|
||||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
localOnly: ps.localOnly,
|
};
|
||||||
reactionAcceptance: ps.reactionAcceptance,
|
|
||||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
|
||||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
|
||||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
const updatedNote = await this.noteUpdateService.update(me, data, note, false);
|
||||||
cw: ps.cw,
|
|
||||||
text: ps.text,
|
return {
|
||||||
});
|
updatedNote: await this.noteEntityService.pack(updatedNote!, me),
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,6 +122,7 @@ const gamingMode = computed(defaultStore.makeGetterSetter('gamingMode'));
|
||||||
document.documentElement.style.setProperty('--homeColor', hexToRgb(defaultStore.state.homeColor));
|
document.documentElement.style.setProperty('--homeColor', hexToRgb(defaultStore.state.homeColor));
|
||||||
document.documentElement.style.setProperty('--followerColor', hexToRgb(defaultStore.state.followerColor));
|
document.documentElement.style.setProperty('--followerColor', hexToRgb(defaultStore.state.followerColor));
|
||||||
document.documentElement.style.setProperty('--specifiedColor', hexToRgb(defaultStore.state.specifiedColor));
|
document.documentElement.style.setProperty('--specifiedColor', hexToRgb(defaultStore.state.specifiedColor));
|
||||||
|
document.documentElement.style.setProperty('--localOnlyColor', hexToRgb(defaultStore.state.localOnlyColor));
|
||||||
document.documentElement.style.setProperty('--gamingspeed', defaultStore.state.numberOfGamingSpeed + 's');
|
document.documentElement.style.setProperty('--gamingspeed', defaultStore.state.numberOfGamingSpeed + 's');
|
||||||
|
|
||||||
let gaming = ref();
|
let gaming = ref();
|
||||||
|
|
Loading…
Reference in New Issue