From 7325d66c52365104b6b5d6343324a258470ad2a8 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Thu, 7 Mar 2019 21:19:32 +0900 Subject: [PATCH] Implement Update Question (#4435) * Update remote votes count * save updatedAt * deliver Update * use renderNote * use id * fix typeof --- src/models/note.ts | 1 + src/queue/processors/http/process-inbox.ts | 6 +- src/remote/activitypub/kernel/index.ts | 5 ++ src/remote/activitypub/kernel/update/index.ts | 28 ++++++++ src/remote/activitypub/models/note.ts | 4 ++ src/remote/activitypub/models/question.ts | 69 +++++++++++++++---- src/remote/activitypub/type.ts | 23 ++++++- src/server/api/endpoints/notes/polls/vote.ts | 4 ++ src/services/note/polls/update.ts | 61 ++++++++++++++++ src/tools/refresh-question.ts | 14 ++++ 10 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 src/remote/activitypub/kernel/update/index.ts create mode 100644 src/services/note/polls/update.ts create mode 100644 src/tools/refresh-question.ts diff --git a/src/models/note.ts b/src/models/note.ts index 369838ae7..549043d55 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -35,6 +35,7 @@ export type INote = { _id: mongo.ObjectID; createdAt: Date; deletedAt: Date; + updatedAt?: Date; fileIds: mongo.ObjectID[]; replyId: mongo.ObjectID; renoteId: mongo.ObjectID; diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index ea737593d..cc4e711d0 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -82,7 +82,7 @@ export default async (job: bq.Job, done: any): Promise => { }) as IRemoteUser; } - // Update activityの場合は、ここで署名検証/更新処理まで実施して終了 + // Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了 if (activity.type === 'Update') { if (activity.object && activity.object.type === 'Person') { if (user == null) { @@ -92,9 +92,9 @@ export default async (job: bq.Job, done: any): Promise => { } else { updatePerson(activity.actor, null, activity.object); } + done(); + return; } - done(); - return; } // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 7cf9ba833..4f7a5c91f 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -2,6 +2,7 @@ import { Object } from '../type'; import { IRemoteUser } from '../../../models/user'; import create from './create'; import performDeleteActivity from './delete'; +import performUpdateActivity from './update'; import follow from './follow'; import undo from './undo'; import like from './like'; @@ -23,6 +24,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise => { await performDeleteActivity(actor, activity); break; + case 'Update': + await performUpdateActivity(actor, activity); + break; + case 'Follow': await follow(actor, activity); break; diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts new file mode 100644 index 000000000..49b730391 --- /dev/null +++ b/src/remote/activitypub/kernel/update/index.ts @@ -0,0 +1,28 @@ +import { IRemoteUser } from '../../../../models/user'; +import { IUpdate, IObject } from '../../type'; +import { apLogger } from '../../logger'; +import { updateQuestion } from '../../models/question'; + +/** + * Updateアクティビティを捌きます + */ +export default async (actor: IRemoteUser, activity: IUpdate): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + apLogger.debug('Update'); + + const object = activity.object as IObject; + + switch (object.type) { + case 'Question': + apLogger.debug('Question'); + await updateQuestion(object).catch(e => console.log(e)); + break; + + default: + apLogger.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 5932d3d90..a87257dff 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -18,6 +18,7 @@ import { extractPollFromQuestion } from './question'; import vote from '../../../services/note/polls/vote'; import { apLogger } from '../logger'; import { IDriveFile } from '../../../models/drive-file'; +import { deliverQuestionUpdate } from '../../../services/note/polls/update'; const logger = apLogger; @@ -136,6 +137,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } else if (index >= 0) { logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await vote(actor, reply, index); + + // リモートフォロワーにUpdate配信 + deliverQuestionUpdate(reply._id); } return null; }; diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index edfd8701b..c07368434 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -1,18 +1,8 @@ -import { IChoice, IPoll } from '../../../models/note'; +import config from '../../../config'; +import Note, { IChoice, IPoll } from '../../../models/note'; import Resolver from '../resolver'; -import { ICollection } from '../type'; - -interface IQuestionChoice { - name?: string; - replies?: ICollection; - _misskey_votes?: number; -} - -interface IQuestion { - oneOf?: IQuestionChoice[]; - anyOf?: IQuestionChoice[]; - endTime?: Date; -} +import { IQuestion } from '../type'; +import { apLogger } from '../logger'; export async function extractPollFromQuestion(source: string | IQuestion): Promise { const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source; @@ -36,3 +26,54 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi expiresAt }; } + +/** + * Update votes of Question + * @param uri URI of AP Question object + * @returns true if updated + */ +export async function updateQuestion(value: any) { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならスキップ + if (uri.startsWith(config.url + '/')) throw 'uri points local'; + + //#region このサーバーに既に登録されているか + const note = await Note.findOne({ uri }); + + if (note == null) throw 'Question is not registed'; + //#endregion + + // resolve new Question object + const resolver = new Resolver(); + const question = await resolver.resolve(value) as IQuestion; + apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); + + if (question.type !== 'Question') throw 'object is not a Question'; + + const apChoices = question.oneOf || question.anyOf; + const dbChoices = note.poll.choices; + + let changed = false; + + for (const db of dbChoices) { + const oldCount = db.votes; + const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems; + + if (oldCount != newCount) { + changed = true; + db.votes = newCount; + } + } + + await Note.update({ + _id: note._id + }, { + $set: { + 'poll.choices': dbChoices, + updatedAt: new Date(), + } + }); + + return changed; +} diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index c8a00f359..c381e6350 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -43,12 +43,28 @@ export interface IOrderedCollection extends IObject { } export interface INote extends IObject { - type: 'Note'; + type: 'Note' | 'Question'; _misskey_content: string; _misskey_quote: string; _misskey_question: string; } +export interface IQuestion extends IObject { + type: 'Note' | 'Question'; + _misskey_content: string; + _misskey_quote: string; + _misskey_question: string; + oneOf?: IQuestionChoice[]; + anyOf?: IQuestionChoice[]; + endTime?: Date; +} + +interface IQuestionChoice { + name?: string; + replies?: ICollection; + _misskey_votes?: number; +} + export interface IPerson extends IObject { type: 'Person'; name: string; @@ -81,6 +97,10 @@ export interface IDelete extends IActivity { type: 'Delete'; } +export interface IUpdate extends IActivity { + type: 'Update'; +} + export interface IUndo extends IActivity { type: 'Undo'; } @@ -123,6 +143,7 @@ export type Object = IOrderedCollection | ICreate | IDelete | + IUpdate | IUndo | IFollow | IAccept | diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index d064c9e53..ed20e0221 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -13,6 +13,7 @@ import { getNote } from '../../../common/getters'; import { deliver } from '../../../../../queue'; import { renderActivity } from '../../../../../remote/activitypub/renderer'; import renderVote from '../../../../../remote/activitypub/renderer/vote'; +import { deliverQuestionUpdate } from '../../../../../services/note/polls/update'; export const meta = { desc: { @@ -172,5 +173,8 @@ export default define(meta, async (ps, user) => { deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox); } + // リモートフォロワーにUpdate配信 + deliverQuestionUpdate(note._id); + return; }); diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts new file mode 100644 index 000000000..d4e183889 --- /dev/null +++ b/src/services/note/polls/update.ts @@ -0,0 +1,61 @@ +import * as mongo from 'mongodb'; +import Note, { INote } from '../../../models/note'; +import { updateQuestion } from '../../../remote/activitypub/models/question'; +import ms = require('ms'); +import Logger from '../../logger'; +import User, { isLocalUser, isRemoteUser } from '../../../models/user'; +import Following from '../../../models/following'; +import renderUpdate from '../../../remote/activitypub/renderer/update'; +import { renderActivity } from '../../../remote/activitypub/renderer'; +import { deliver } from '../../../queue'; +import renderNote from '../../../remote/activitypub/renderer/note'; + +const logger = new Logger('pollsUpdate'); + +export async function triggerUpdate(note: INote) { + if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) { + logger.info(`Updating ${note._id}`); + + try { + const updated = await updateQuestion(note.uri); + logger.info(`Updated ${note._id} ${updated ? 'changed' : 'nochange'}`); + } catch (e) { + logger.error(e); + } + } +} + +export async function deliverQuestionUpdate(noteId: mongo.ObjectID) { + const note = await Note.findOne({ + _id: noteId, + }); + + const user = await User.findOne({ + _id: note.userId + }); + + const followers = await Following.find({ + followeeId: user._id + }); + + const queue: string[] = []; + + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 + if (isLocalUser(user)) { + for (const following of followers) { + const follower = following._follower; + + if (isRemoteUser(follower)) { + const inbox = follower.sharedInbox || follower.inbox; + if (!queue.includes(inbox)) queue.push(inbox); + } + } + + if (queue.length > 0) { + const content = renderActivity(renderUpdate(await renderNote(note, false), user)); + for (const inbox of queue) { + deliver(user, content, inbox); + } + } + } +} diff --git a/src/tools/refresh-question.ts b/src/tools/refresh-question.ts new file mode 100644 index 000000000..83d71ff30 --- /dev/null +++ b/src/tools/refresh-question.ts @@ -0,0 +1,14 @@ +import { updateQuestion } from '../remote/activitypub/models/question'; + +async function main(uri: string): Promise { + return await updateQuestion(uri); +} + +const args = process.argv.slice(2); +const uri = args[0]; + +main(uri).then(result => { + console.log(`Done: ${result}`); +}).catch(e => { + console.warn(e); +});