summaryrefslogtreecommitdiff
path: root/src/remote
diff options
context:
space:
mode:
authorAcid Chicken (硫酸鶏) <root@acid-chicken.com>2019-03-06 22:55:47 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2019-03-06 22:55:47 +0900
commit725600da8f92a223f10a4a9a1ff874c5eff1534f (patch)
tree1797d5561f71fb2a67b31fb4f79611f8f4ab04dd /src/remote
parent[Client] Fix bug (diff)
downloadsharkey-725600da8f92a223f10a4a9a1ff874c5eff1534f.tar.gz
sharkey-725600da8f92a223f10a4a9a1ff874c5eff1534f.tar.bz2
sharkey-725600da8f92a223f10a4a9a1ff874c5eff1534f.zip
Enhance poll (#4409)
* 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 <m@m544.net> Date: Wed Mar 6 05:16:14 2019 +0900 tune commit 83ff421a6e978243f80ba9ec820189bc897e6e3b Author: mei23 <m@m544.net> Date: Wed Mar 6 05:01:14 2019 +0900 fallback commit 0b566af973b115ade9e75ea4b8094ee2b329dabc Author: mei23 <m@m544.net> Date: Wed Mar 6 04:40:12 2019 +0900 Note commit cc0296dd6127580ac584c40398db3f762a311f8b Author: mei23 <m@m544.net> Date: Wed Mar 6 04:33:58 2019 +0900 createで送る * Squashed commit of the following: commit ae696b1ed12568b27c27367ac5a77035c97c9a1f Author: mei23 <m@m544.net> Date: Wed Mar 6 06:11:17 2019 +0900 fix commit b735e354e7a9e64534c4f17d04ecbc65fb735c21 Author: mei23 <m@m544.net> Date: Wed Mar 6 06:08:33 2019 +0900 messge commit d9a4beabf851893b8992a0f4568265eb9d4f0b8e Author: mei23 <m@m544.net> Date: Wed Mar 6 05:16:14 2019 +0900 tune commit 83ff421a6e978243f80ba9ec820189bc897e6e3b Author: mei23 <m@m544.net> Date: Wed Mar 6 05:01:14 2019 +0900 fallback commit 0b566af973b115ade9e75ea4b8094ee2b329dabc Author: mei23 <m@m544.net> Date: Wed Mar 6 04:40:12 2019 +0900 Note commit cc0296dd6127580ac584c40398db3f762a311f8b Author: mei23 <m@m544.net> 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
Diffstat (limited to 'src/remote')
-rw-r--r--src/remote/activitypub/kernel/announce/index.ts4
-rw-r--r--src/remote/activitypub/kernel/create/index.ts6
-rw-r--r--src/remote/activitypub/kernel/delete/index.ts4
-rw-r--r--src/remote/activitypub/models/note.ts49
-rw-r--r--src/remote/activitypub/models/question.ts41
-rw-r--r--src/remote/activitypub/renderer/note.ts29
-rw-r--r--src/remote/activitypub/renderer/question.ts18
-rw-r--r--src/remote/activitypub/renderer/vote.ts22
-rw-r--r--src/remote/activitypub/type.ts4
9 files changed, 142 insertions, 35 deletions
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<void> =>
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<void> => {
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<void> => {
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<INote> {
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<IDriveFile>))
@@ -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<null> => {
+ 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<IPoll> {
- 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<IPoll> {
+ 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<any>
: 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<any>
...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<any>
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<any> {
+ 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;