From 11496d887e89ceccd64035f9e1836c5d415f4349 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Tue, 18 Sep 2018 13:08:27 +0900 Subject: [PATCH] Publish pinned notes (#2731) --- src/remote/activitypub/renderer/add.ts | 9 +++ .../renderer/ordered-collection.ts | 4 +- src/remote/activitypub/renderer/person.ts | 1 + src/remote/activitypub/renderer/remove.ts | 9 +++ src/remote/activitypub/type.ts | 1 + src/server/activitypub.ts | 4 ++ src/server/activitypub/featured.ts | 38 ++++++++++++ src/server/api/endpoints/i/pin.ts | 10 ++- src/services/i/pin.ts | 61 +++++++++++++++++++ 9 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/remote/activitypub/renderer/add.ts create mode 100644 src/remote/activitypub/renderer/remove.ts create mode 100644 src/server/activitypub/featured.ts create mode 100644 src/services/i/pin.ts diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts new file mode 100644 index 000000000..4d6fe392a --- /dev/null +++ b/src/remote/activitypub/renderer/add.ts @@ -0,0 +1,9 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser, target: any, object: any) => ({ + type: 'Add', + actor: `${config.url}/users/${user._id}`, + target, + object +}); diff --git a/src/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts index 3c448cf87..546100598 100644 --- a/src/remote/activitypub/renderer/ordered-collection.ts +++ b/src/remote/activitypub/renderer/ordered-collection.ts @@ -4,8 +4,9 @@ * @param totalItems Total number of items * @param first URL of first page (optional) * @param last URL of last page (optional) + * @param orderedItems attached objects (optional) */ -export default function(id: string, totalItems: any, first: string, last: string) { +export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) { const page: any = { id, type: 'OrderedCollection', @@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string if (first) page.first = first; if (last) page.last = last; + if (orderedItems) page.orderedItems = orderedItems; return page; } diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 78918af36..52485e695 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -21,6 +21,7 @@ export default async (user: ILocalUser) => { outbox: `${id}/outbox`, followers: `${id}/followers`, following: `${id}/following`, + featured: `${id}/collections/featured`, sharedInbox: `${config.url}/inbox`, url: `${config.url}/@${user.username}`, preferredUsername: user.username, diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts new file mode 100644 index 000000000..ed840be75 --- /dev/null +++ b/src/remote/activitypub/renderer/remove.ts @@ -0,0 +1,9 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser, target: any, object: any) => ({ + type: 'Remove', + actor: `${config.url}/users/${user._id}`, + target, + object +}); diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 28763d3e8..7bbea5fd1 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -53,6 +53,7 @@ export interface IPerson extends IObject { publicKey: any; followers: any; following: any; + featured?: any; outbox: any; endpoints: string[]; } diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index 3d346693d..2cbce8cfb 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person'; import Outbox, { packActivity } from './activitypub/outbox'; import Followers from './activitypub/followers'; import Following from './activitypub/following'; +import Featured from './activitypub/featured'; // Init router const router = new Router(); @@ -102,6 +103,9 @@ router.get('/users/:user/followers', Followers); // following router.get('/users/:user/following', Following); +// featured +router.get('/users/:user/collections/featured', Featured); + // publickey router.get('/users/:user/publickey', async ctx => { const userId = new mongo.ObjectID(ctx.params.user); diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts new file mode 100644 index 000000000..93c370020 --- /dev/null +++ b/src/server/activitypub/featured.ts @@ -0,0 +1,38 @@ +import * as mongo from 'mongodb'; +import * as Router from 'koa-router'; +import config from '../../config'; +import User from '../../models/user'; +import pack from '../../remote/activitypub/renderer'; +import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; +import { setResponseType } from '../activitypub'; +import Note from '../../models/note'; +import renderNote from '../../remote/activitypub/renderer/note'; + +export default async (ctx: Router.IRouterContext) => { + const userId = new mongo.ObjectID(ctx.params.user); + + // Verify user + const user = await User.findOne({ + _id: userId, + host: null + }); + + if (user === null) { + ctx.status = 404; + return; + } + + const pinnedNoteIds = user.pinnedNoteIds || []; + + const pinnedNotes = await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id }))); + + const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note))); + + const rendered = renderOrderedCollection( + `${config.url}/users/${userId}/collections/featured`, + renderedNotes.length, null, null, renderedNotes + ); + + ctx.body = pack(rendered); + setResponseType(ctx); +}; diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index 8804d5aa7..ce3b0318a 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -2,6 +2,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import User, { ILocalUser } from '../../../../models/user'; import Note from '../../../../models/note'; import { pack } from '../../../../models/user'; +import { deliverPinnedChange } from '../../../../services/i/pin'; /** * Pin note @@ -21,6 +22,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, return rej('note not found'); } + let addedId; + let removedId; + const pinnedNoteIds = user.pinnedNoteIds || []; if (pinnedNoteIds.some(id => id.equals(note._id))) { @@ -28,9 +32,10 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, } pinnedNoteIds.unshift(note._id); + addedId = note._id; if (pinnedNoteIds.length > 5) { - pinnedNoteIds.pop(); + removedId = pinnedNoteIds.pop(); } await User.update(user._id, { @@ -44,6 +49,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, detail: true }); + // Send Add/Remove to followers + deliverPinnedChange(user._id, removedId, addedId); + // Send response res(iObj); }); diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts new file mode 100644 index 000000000..c6d3e1178 --- /dev/null +++ b/src/services/i/pin.ts @@ -0,0 +1,61 @@ +import config from '../../config'; +import * as mongo from 'mongodb'; +import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user'; +import Following from '../../models/following'; +import renderAdd from '../../remote/activitypub/renderer/add'; +import renderRemove from '../../remote/activitypub/renderer/remove'; +import packAp from '../../remote/activitypub/renderer'; +import { deliver } from '../../queue'; + +export async function deliverPinnedChange(userId: mongo.ObjectID, oldId: mongo.ObjectID, newId: mongo.ObjectID) { + const user = await User.findOne({ + _id: userId + }); + + if (!isLocalUser(user)) return; + + const queue = await CreateRemoteInboxes(user); + + if (queue.length < 1) return; + + const target = `${config.url}/users/${user._id}/collections/featured`; + + if (oldId) { + const oldItem = `${config.url}/notes/${oldId}`; + const content = packAp(renderRemove(user, target, oldItem)); + queue.forEach(inbox => { + deliver(user, content, inbox); + }); + } + + if (newId) { + const newItem = `${config.url}/notes/${newId}`; + const content = packAp(renderAdd(user, target, newItem)); + queue.forEach(inbox => { + deliver(user, content, inbox); + }); + } +} + +/** + * ローカルユーザーのリモートフォロワーのinboxリストを作成する + * @param user ローカルユーザー + */ +async function CreateRemoteInboxes(user: ILocalUser): Promise { + const followers = await Following.find({ + followeeId: user._id + }); + + const queue: string[] = []; + + followers.map(following => { + const follower = following._follower; + + if (isRemoteUser(follower)) { + const inbox = follower.sharedInbox || follower.inbox; + if (!queue.includes(inbox)) queue.push(inbox); + } + }); + + return queue; +}