Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

リモートで投票を見たりしたりできるように #3940

Merged
merged 30 commits into from
Jan 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/models/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ export type INote = {
fileIds: mongo.ObjectID[];
replyId: mongo.ObjectID;
renoteId: mongo.ObjectID;
poll: {
choices: Array<{
id: number;
}>
};
poll: IPoll;
text: string;
tags: string[];
tagsLower: string[];
Expand Down Expand Up @@ -102,6 +98,16 @@ export type INote = {
_files?: IDriveFile[];
};

export type IPoll = {
choices: IChoice[]
};

export type IChoice = {
id: number;
text: string;
votes: number;
};

export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
let hide = false;

Expand Down
17 changes: 17 additions & 0 deletions src/remote/activitypub/models/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import Emoji, { IEmoji } from '../../../models/emoji';
import { ITag } from './tag';
import { toUnicode } from 'punycode';
import { unique, concat, difference } from '../../../prelude/array';
import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';

const log = debug('misskey:activitypub');

Expand Down Expand Up @@ -110,13 +112,26 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// テキストのパース
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);

// vote
if (reply && reply.poll && text != null) {
const m = text.match(/([0-9])$/);
if (m) {
log(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${m[0]}`);
await vote(actor, reply, Number(m[1]));
return null;
}
}

const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
console.log(`extractEmojis: ${e}`);
return [] as IEmoji[];
});

const apEmojis = emojis.map(emoji => emoji.name);

const questionUri = note._misskey_question;
const poll = questionUri ? await extractPollFromQuestion(questionUri).catch(() => undefined) : undefined;

// ユーザーの情報が古かったらついでに更新しておく
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
updatePerson(note.attributedTo);
Expand All @@ -137,6 +152,8 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
apMentions,
apHashtags,
apEmojis,
questionUri,
poll,
uri: note.id
}, silent);
}
Expand Down
19 changes: 19 additions & 0 deletions src/remote/activitypub/models/question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IChoice, IPoll } from '../../../models/note';
import Resolver from '../resolver';

export async function extractPollFromQuestion(questionUri: string): Promise<IPoll> {
const resolver = new Resolver();
const question = await resolver.resolve(questionUri) as any;

const choices: IChoice[] = question.oneOf.map((x: any, i: number) => {
return {
id: i,
text: x.name,
votes: x._misskey_votes || 0,
} as IChoice;
});

return {
choices
};
}
15 changes: 13 additions & 2 deletions src/remote/activitypub/renderer/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,27 @@ export default async function renderNote(note: INote, dive = true): Promise<any>

let text = note.text;

let question: string;
if (note.poll != null) {
if (text == null) text = '';
const url = `${config.url}/notes/${note._id}`;
// TODO: i18n
text += `\n\n[投票を見る](${url})`;
text += `\n\n[リモートで投票を見る](${url})`;

question = `${config.url}/questions/${note._id}`;
}

let apText = text;
if (apText == null) apText = '';

// Provides choices as text for AP
if (note.poll != null) {
const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`);
apText += '\n';
apText += cs.join('\n');
}

if (quote) {
if (apText == null) apText = '';
apText += `\n\nRE: ${quote}`;
}

Expand All @@ -130,6 +140,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
content,
_misskey_content: text,
_misskey_quote: quote,
_misskey_question: question,
published: note.createdAt.toISOString(),
to,
cc,
Expand Down
20 changes: 20 additions & 0 deletions src/remote/activitypub/renderer/question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { INote } from '../../../models/note';

export default async function renderQuestion(user: ILocalUser, note: INote) {
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,
};
}),
};

return question;
}
1 change: 1 addition & 0 deletions src/remote/activitypub/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface INote extends IObject {
type: 'Note';
_misskey_content: string;
_misskey_quote: string;
_misskey_question: string;
}

export interface IPerson extends IObject {
Expand Down
31 changes: 31 additions & 0 deletions src/server/activitypub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
import Featured from './activitypub/featured';
import renderQuestion from '../remote/activitypub/renderer/question';

// Init router
const router = new Router();
Expand Down Expand Up @@ -110,6 +111,36 @@ router.get('/notes/:note/activity', async ctx => {
setResponseType(ctx);
});

// question
router.get('/questions/:question', async (ctx, next) => {
if (!ObjectID.isValid(ctx.params.question)) {
ctx.status = 404;
return;
}

const poll = await Note.findOne({
_id: new ObjectID(ctx.params.question),
visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true },
poll: {
$exists: true,
$ne: null
},
});

if (poll === null) {
ctx.status = 404;
return;
}

const user = await User.findOne({
_id: poll.userId
});

ctx.body = pack(await renderQuestion(user as ILocalUser, poll));
setResponseType(ctx);
});

// outbox
router.get('/users/:user/outbox', Outbox);

Expand Down
17 changes: 17 additions & 0 deletions src/server/api/endpoints/notes/polls/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import watch from '../../../../../services/note/watch';
import { publishNoteStream } from '../../../../../stream';
import notify from '../../../../../notify';
import define from '../../../define';
import createNote from '../../../../../services/note/create';
import User from '../../../../../models/user';

export const meta = {
desc: {
Expand Down Expand Up @@ -114,4 +116,19 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
if (user.settings.autoWatch !== false) {
watch(user._id, note);
}

// リモート投票の場合リプライ送信
if (note._user.host != null) {
const pollOwner = await User.findOne({
_id: note.userId
});

createNote(user, {
createdAt: new Date(),
text: ps.choice.toString(),
reply: note,
visibility: 'specified',
visibleUsers: [ pollOwner ],
});
}
}));
1 change: 1 addition & 0 deletions src/services/note/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type Option = {
apMentions?: IUser[];
apHashtags?: string[];
apEmojis?: string[];
questionUri?: string;
uri?: string;
app?: IApp;
};
Expand Down
78 changes: 78 additions & 0 deletions src/services/note/polls/vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Vote from '../../../models/poll-vote';
import Note, { INote } from '../../../models/note';
import Watching from '../../../models/note-watching';
import watch from '../../../services/note/watch';
import { publishNoteStream } from '../../../stream';
import notify from '../../../notify';
import createNote from '../../../services/note/create';
import { isLocalUser, IUser } from '../../../models/user';

export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => {
if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param');

// if already voted
const exist = await Vote.findOne({
noteId: note._id,
userId: user._id
});

if (exist !== null) {
return rej('already voted');
}

// Create vote
await Vote.insert({
createdAt: new Date(),
noteId: note._id,
userId: user._id,
choice: choice
});

// Send response
res();

const inc: any = {};
inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1;

// Increment votes count
await Note.update({ _id: note._id }, {
$inc: inc
});

publishNoteStream(note._id, 'pollVoted', {
choice: choice,
userId: user._id.toHexString()
});

// Notify
notify(note.userId, user._id, 'poll_vote', {
noteId: note._id,
choice: choice
});

// Fetch watchers
Watching
.find({
noteId: note._id,
userId: { $ne: user._id },
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
}, {
fields: {
userId: true
}
})
.then(watchers => {
for (const watcher of watchers) {
notify(watcher.userId, user._id, 'poll_vote', {
noteId: note._id,
choice: choice
});
}
});

// ローカルユーザーが投票した場合この投稿をWatchする
if (isLocalUser(user) && user.settings.autoWatch !== false) {
watch(user._id, note);
}
});