From 725600da8f92a223f10a4a9a1ff874c5eff1534f Mon Sep 17 00:00:00 2001 From: "Acid Chicken (硫酸鶏)" Date: Wed, 6 Mar 2019 22:55:47 +0900 Subject: Enhance poll (#4409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Start working * WIP: Enhance poll * Fix bug * Use `name` in voting note refs: https://github.com/syuilo/misskey/issues/4407#issuecomment-469057296 * Fix style * Refactor Co-authored-by: MeiMei <30769358+mei23@users.noreply.github.com> * WIP: Update poll editor * Fix bug * Fix bug refs: https://github.com/syuilo/misskey/pull/4409#discussion_r * Fix typo * Better design * Beautify poll editor * Fix UI * Fix bug refs: https://github.com/syuilo/misskey/pull/4409#discussion_r262217524 * Add debug logging * Fix bug * Log deliver * fix vote * Update ap/show refs: https://github.com/syuilo/misskey/pull/4409#issuecomment-469652386 * Update poll view * Maybe done * Add tests * Fix path * Fix test * Fix test * Fix test * Fix expired check on AP * Update note.ts * Squashed commit of the following: commit d9a4beabf851893b8992a0f4568265eb9d4f0b8e Author: mei23 Date: Wed Mar 6 05:16:14 2019 +0900 tune commit 83ff421a6e978243f80ba9ec820189bc897e6e3b Author: mei23 Date: Wed Mar 6 05:01:14 2019 +0900 fallback commit 0b566af973b115ade9e75ea4b8094ee2b329dabc Author: mei23 Date: Wed Mar 6 04:40:12 2019 +0900 Note commit cc0296dd6127580ac584c40398db3f762a311f8b Author: mei23 Date: Wed Mar 6 04:33:58 2019 +0900 createで送る * Squashed commit of the following: commit ae696b1ed12568b27c27367ac5a77035c97c9a1f Author: mei23 Date: Wed Mar 6 06:11:17 2019 +0900 fix commit b735e354e7a9e64534c4f17d04ecbc65fb735c21 Author: mei23 Date: Wed Mar 6 06:08:33 2019 +0900 messge commit d9a4beabf851893b8992a0f4568265eb9d4f0b8e Author: mei23 Date: Wed Mar 6 05:16:14 2019 +0900 tune commit 83ff421a6e978243f80ba9ec820189bc897e6e3b Author: mei23 Date: Wed Mar 6 05:01:14 2019 +0900 fallback commit 0b566af973b115ade9e75ea4b8094ee2b329dabc Author: mei23 Date: Wed Mar 6 04:40:12 2019 +0900 Note commit cc0296dd6127580ac584c40398db3f762a311f8b Author: mei23 Date: Wed Mar 6 04:33:58 2019 +0900 createで送る * Fix typo * Update vote.ts * Update vote.ts * Update poll-editor.vue * Update tslint.json * Fix layout * Add note * Fix bug * Rename text key * 投票するときに投稿として扱わないように (#4425) * wip * 形式をMastodonと合わせた * Bye something * Use - instead of ~ * Redundancy * Yes! * Refactor * Use moment instead of Date * Fix indent * Refactor if (votes.length) は必要なさそう * Clean up * Bye Date * Clean * Fix timer is not displayed * Fix リモートから無期限pollにvoteできない * Fix vote actor --- src/remote/activitypub/kernel/announce/index.ts | 4 ++ src/remote/activitypub/kernel/create/index.ts | 6 ++- src/remote/activitypub/kernel/delete/index.ts | 4 ++ src/remote/activitypub/models/note.ts | 49 ++++++++++++++++++------- src/remote/activitypub/models/question.ts | 41 +++++++++++++++------ src/remote/activitypub/renderer/note.ts | 29 ++++++++++++++- src/remote/activitypub/renderer/question.ts | 18 +++++---- src/remote/activitypub/renderer/vote.ts | 22 +++++++++++ src/remote/activitypub/type.ts | 4 ++ 9 files changed, 142 insertions(+), 35 deletions(-) create mode 100644 src/remote/activitypub/renderer/vote.ts (limited to 'src/remote') diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index 80875b90da..3b2eeb7aa2 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -27,6 +27,10 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise => announceNote(resolver, actor, activity, object as INote); break; + case 'Question': + announceNote(resolver, actor, activity, object as INote); + break; + default: logger.warn(`Unknown announce type: ${object.type}`); break; diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index c633d95487..2afdc01377 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -1,7 +1,7 @@ import Resolver from '../../resolver'; import { IRemoteUser } from '../../../../models/user'; -import createNote from './note'; import createImage from './image'; +import createNote from './note'; import { ICreate } from '../../type'; import { apLogger } from '../../logger'; @@ -32,6 +32,10 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise => { createNote(resolver, actor, object); break; + case 'Question': + createNote(resolver, actor, object); + break; + default: logger.warn(`Unknown type: ${object.type}`); break; diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts index eead34785c..864c9f5f7d 100644 --- a/src/remote/activitypub/kernel/delete/index.ts +++ b/src/remote/activitypub/kernel/delete/index.ts @@ -24,6 +24,10 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise => { deleteNote(actor, uri); break; + case 'Question': + deleteNote(actor, uri); + break; + case 'Tombstone': const note = await Note.findOne({ uri }); if (note != null) { diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 76b66a07c3..5932d3d90e 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -52,9 +52,9 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P export async function createNote(value: any, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = new Resolver(); - const object = await resolver.resolve(value) as any; + const object: any = await resolver.resolve(value); - if (object == null || object.type !== 'Note') { + if (!object || !['Note', 'Question'].includes(object.type)) { logger.error(`invalid note: ${value}`, { resolver: { history: resolver.getHistory() @@ -67,6 +67,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const note: INoteActivityStreamsObject = object; + logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); + logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ @@ -78,6 +80,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } //#region Visibility + note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to; + note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc; + let visibility = 'public'; let visibleUsers: IUser[] = []; if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) { @@ -89,7 +94,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false visibility = 'specified'; visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver))); } - } +} //#endergion const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); @@ -101,6 +106,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false // TODO: attachmentは必ずしも配列ではない // Noteがsensitiveなら添付もsensitiveにする const limit = promiseLimit(2); + + note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; const files = note.attachment .map(attach => attach.sensitive = note.sensitive) ? await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise)) @@ -119,15 +126,31 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const cw = note.summary === '' ? null : note.summary; // テキストのパース - const text = note._misskey_content ? note._misskey_content : fromHtml(note.content); + const text = note._misskey_content || fromHtml(note.content); // vote - if (reply && reply.poll && text != null) { - const m = text.match(/([0-9])$/); - if (m) { - logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${m[0]}`); - await vote(actor, reply, Number(m[1])); + if (reply && reply.poll) { + const tryCreateVote = async (name: string, index: number): Promise => { + if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) { + logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); + } else if (index >= 0) { + logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); + await vote(actor, reply, index); + } return null; + }; + + if (note.name) { + return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name)); + } + + // 後方互換性のため + if (text) { + const m = text.match(/(\d+)$/); + + if (m) { + return await tryCreateVote(m[0], Number(m[1])); + } } } @@ -139,7 +162,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const apEmojis = emojis.map(emoji => emoji.name); const questionUri = note._misskey_question; - const poll = questionUri ? await extractPollFromQuestion(questionUri).catch(() => undefined) : undefined; + const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined); // ユーザーの情報が古かったらついでに更新しておく if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { @@ -148,11 +171,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false return await post(actor, { createdAt: new Date(note.published), - files: files, + files, reply, renote: quote, - cw: cw, - text: text, + cw, + text, viaMobile: false, localOnly: false, geo: undefined, diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index 53892a409e..edfd8701b4 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -1,19 +1,38 @@ import { IChoice, IPoll } from '../../../models/note'; import Resolver from '../resolver'; +import { ICollection } from '../type'; -export async function extractPollFromQuestion(questionUri: string): Promise { - const resolver = new Resolver(); - const question = await resolver.resolve(questionUri) as any; +interface IQuestionChoice { + name?: string; + replies?: ICollection; + _misskey_votes?: number; +} + +interface IQuestion { + oneOf?: IQuestionChoice[]; + anyOf?: IQuestionChoice[]; + endTime?: Date; +} + +export async function extractPollFromQuestion(source: string | IQuestion): Promise { + const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source; + const multiple = !question.oneOf; + const expiresAt = question.endTime ? new Date(question.endTime) : null; + + if (multiple && !question.anyOf) { + throw 'invalid question'; + } - const choices: IChoice[] = question.oneOf.map((x: any, i: number) => { - return { - id: i, - text: x.name, - votes: x._misskey_votes || 0, - } as IChoice; - }); + const choices = question[multiple ? 'anyOf' : 'oneOf'] + .map((x, i) => ({ + id: i, + text: x.name, + votes: x.replies && x.replies.totalItems || x._misskey_votes || 0, + } as IChoice)); return { - choices + choices, + multiple, + expiresAt }; } diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 910e4dba76..8b349526e1 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -15,9 +15,10 @@ export default async function renderNote(note: INote, dive = true): Promise : Promise.resolve([]); let inReplyTo; + let inReplyToNote: INote; if (note.replyId) { - const inReplyToNote = await Note.findOne({ + inReplyToNote = await Note.findOne({ _id: note.replyId, }); @@ -134,6 +135,29 @@ export default async function renderNote(note: INote, dive = true): Promise ...apemojis, ]; + const { + choices = [], + expiresAt = null, + multiple = false + } = note.poll || {}; + + const asPoll = note.poll ? { + type: 'Question', + content: toHtml(Object.assign({}, note, { + text: text + })), + _misskey_fallback_content: content, + [expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt, + [multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({ + type: 'Note', + name: text, + replies: { + type: 'Collection', + totalItems: votes + } + })) + } : {}; + return { id: `${config.url}/notes/${note._id}`, type: 'Note', @@ -149,7 +173,8 @@ export default async function renderNote(note: INote, dive = true): Promise inReplyTo, attachment: files.map(renderDocument), sensitive: files.some(file => file.metadata.isSensitive), - tag + tag, + ...asPoll }; } diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts index 9df4daca3b..cf0bf387c8 100644 --- a/src/remote/activitypub/renderer/question.ts +++ b/src/remote/activitypub/renderer/question.ts @@ -3,17 +3,19 @@ import { ILocalUser } from '../../../models/user'; import { INote } from '../../../models/note'; export default async function renderQuestion(user: ILocalUser, note: INote) { - const question = { + const question = { type: 'Question', id: `${config.url}/questions/${note._id}`, actor: `${config.url}/users/${user._id}`, - content: note.text != null ? note.text : '', - oneOf: note.poll.choices.map(c => { - return { - name: c.text, - _misskey_votes: c.votes, - }; - }), + content: note.text || '', + [note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({ + name: c.text, + _misskey_votes: c.votes, + replies: { + type: 'Collection', + totalItems: c.votes + } + })) }; return question; diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts new file mode 100644 index 0000000000..014b76765b --- /dev/null +++ b/src/remote/activitypub/renderer/vote.ts @@ -0,0 +1,22 @@ +import config from '../../../config'; +import { INote } from '../../../models/note'; +import { IRemoteUser, ILocalUser } from '../../../models/user'; +import { IPollVote } from '../../../models/poll-vote'; + +export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise { + return { + id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`, + actor: `${config.url}/users/${user._id}`, + type: 'Create', + to: [pollOwner.uri], + published: new Date().toISOString(), + object: { + id: `${config.url}/users/${user._id}#votes/${vote._id}`, + type: 'Note', + attributedTo: `${config.url}/users/${user._id}`, + to: [pollOwner.uri], + inReplyTo: pollNote.uri, + name: pollNote.poll.choices.find(x => x.id === vote.choice).text + } + }; +} diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index b902abea23..c8a00f3591 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -11,7 +11,11 @@ export interface IObject { attributedTo: string; attachment?: any[]; inReplyTo?: any; + replies?: ICollection; content: string; + name?: string; + startTime?: Date; + endTime?: Date; icon?: any; image?: any; url?: string; -- cgit v1.2.3-freya