diff --git a/src/models/user.ts b/src/models/user.ts index 8ff91d3f45..d2124bda74 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -113,6 +113,7 @@ export interface ILocalUser extends IUserBase { export interface IRemoteUser extends IUserBase { inbox: string; sharedInbox?: string; + featured?: string; endpoints: string[]; uri: string; url?: string; diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts new file mode 100644 index 0000000000..eb2dba5b21 --- /dev/null +++ b/src/remote/activitypub/kernel/add/index.ts @@ -0,0 +1,22 @@ +import { IRemoteUser } from '../../../../models/user'; +import { IAdd } from '../../type'; +import { resolveNote } from '../../models/note'; +import { addPinned } from '../../../../services/i/pin'; + +export default async (actor: IRemoteUser, activity: IAdd): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + if (activity.target == null) { + throw new Error('target is null'); + } + + if (activity.target === actor.featured) { + const note = await resolveNote(activity.object); + await addPinned(actor, note._id); + return; + } + + throw new Error(`unknown target: ${activity.target}`); +}; diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 752a9bd2e2..52b0efc730 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -8,6 +8,8 @@ import like from './like'; import announce from './announce'; import accept from './accept'; import reject from './reject'; +import add from './add'; +import remove from './remove'; const self = async (actor: IRemoteUser, activity: Object): Promise => { switch (activity.type) { @@ -31,6 +33,14 @@ const self = async (actor: IRemoteUser, activity: Object): Promise => { await reject(actor, activity); break; + case 'Add': + await add(actor, activity).catch(err => console.log(err)); + break; + + case 'Remove': + await remove(actor, activity).catch(err => console.log(err)); + break; + case 'Announce': await announce(actor, activity); break; diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts new file mode 100644 index 0000000000..91b207c80d --- /dev/null +++ b/src/remote/activitypub/kernel/remove/index.ts @@ -0,0 +1,22 @@ +import { IRemoteUser } from '../../../../models/user'; +import { IRemove } from '../../type'; +import { resolveNote } from '../../models/note'; +import { removePinned } from '../../../../services/i/pin'; + +export default async (actor: IRemoteUser, activity: IRemove): Promise => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + if (activity.target == null) { + throw new Error('target is null'); + } + + if (activity.target === actor.featured) { + const note = await resolveNote(activity.object); + await removePinned(actor, note._id); + return; + } + + throw new Error(`unknown target: ${activity.target}`); +}; diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index b4afda765a..d49cf53079 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -56,7 +56,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false log(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(note.attributedTo) as IRemoteUser; + const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -73,7 +73,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false visibility = 'followers'; } else { visibility = 'specified'; - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); + visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver))); } } //#endergion diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index dff38f5460..ee95e43ad3 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -3,15 +3,16 @@ import { toUnicode } from 'punycode'; import * as debug from 'debug'; import config from '../../../config'; -import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; +import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user'; import Resolver from '../resolver'; import { resolveImage } from './image'; -import { isCollectionOrOrderedCollection, IPerson } from '../type'; +import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; import { IDriveFile } from '../../../models/drive-file'; import Meta from '../../../models/meta'; import htmlToMFM from '../../../mfm/html-to-mfm'; import { updateUserStats } from '../../../services/update-chart'; import { URL } from 'url'; +import { resolveNote } from './note'; const log = debug('misskey:activitypub'); @@ -155,6 +156,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise console.log(err)); return user; } @@ -282,6 +285,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje updatedAt: new Date(), inbox: person.inbox, sharedInbox: person.sharedInbox, + featured: person.featured, avatarId: avatar ? avatar._id : null, bannerId: banner ? banner._id : null, avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null, @@ -303,6 +307,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje }, } }); + + await updateFeatured(exist._id).catch(err => console.log(err)); } /** @@ -311,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(uri: string, verifier?: string): Promise { +export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw 'uri is not string'; //#region このサーバーに既に登録されていたらそれを返す @@ -323,5 +329,37 @@ export async function resolvePerson(uri: string, verifier?: string): Promise item.type === 'Note') + .slice(0, 5) + .map(item => resolveNote(item, resolver))); + + await User.update({ _id: user._id }, { + $set: { + pinnedNoteIds: featuredNotes.map(note => note._id) + } + }); } diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index e1c12e7e62..215e5e8704 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -19,11 +19,11 @@ export default class Resolver { switch (collection.type) { case 'Collection': - collection.objects = collection.object.items; + collection.objects = collection.items; break; case 'OrderedCollection': - collection.objects = collection.object.orderedItems; + collection.objects = collection.orderedItems; break; default: diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 7bbea5fd18..5c06ee4ffe 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -91,6 +91,14 @@ export interface IReject extends IActivity { type: 'Reject'; } +export interface IAdd extends IActivity { + type: 'Add'; +} + +export interface IRemove extends IActivity { + type: 'Remove'; +} + export interface ILike extends IActivity { type: 'Like'; _misskey_reaction: string; @@ -109,5 +117,7 @@ export type Object = IFollow | IAccept | IReject | + IAdd | + IRemove | ILike | IAnnounce; diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index f9ae032b11..bf729ca091 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import User, { ILocalUser } from '../../../../models/user'; -import Note from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; import { pack } from '../../../../models/user'; -import { deliverPinnedChange } from '../../../../services/i/pin'; +import { addPinned } from '../../../../services/i/pin'; import getParams from '../../get-params'; export const meta = { @@ -27,41 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, const [ps, psErr] = getParams(meta, params); if (psErr) return rej(psErr); - // Fetch pinee - const note = await Note.findOne({ - _id: ps.noteId, - userId: user._id - }); - - if (note === null) { - return rej('note not found'); + // Processing + try { + await addPinned(user, ps.noteId); + } catch (e) { + return rej(e.message); } - const pinnedNoteIds = user.pinnedNoteIds || []; - - if (pinnedNoteIds.length > 5) { - return rej('cannot pin more notes'); - } - - if (pinnedNoteIds.some(id => id.equals(note._id))) { - return rej('already exists'); - } - - pinnedNoteIds.unshift(note._id); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } - }); - + // Serialize const iObj = await pack(user, user, { detail: true }); // Send response res(iObj); - - // Send Add to followers - deliverPinnedChange(user._id, note._id, true); }); diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts index 82625ae5fb..2a81993e4b 100644 --- a/src/server/api/endpoints/i/unpin.ts +++ b/src/server/api/endpoints/i/unpin.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; -import User, { ILocalUser } from '../../../../models/user'; -import Note from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; import { pack } from '../../../../models/user'; -import { deliverPinnedChange } from '../../../../services/i/pin'; +import { removePinned } from '../../../../services/i/pin'; import getParams from '../../get-params'; export const meta = { @@ -27,31 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, const [ps, psErr] = getParams(meta, params); if (psErr) return rej(psErr); - // Fetch unpinee - const note = await Note.findOne({ - _id: ps.noteId, - userId: user._id - }); - - if (note === null) { - return rej('note not found'); + // Processing + try { + await removePinned(user, ps.noteId); + } catch (e) { + return rej(e.message); } - const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } - }); - + // Serialize const iObj = await pack(user, user, { detail: true }); // Send response res(iObj); - - // Send Remove to followers - deliverPinnedChange(user._id, note._id, false); }); diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts index 8b7287e68d..a39cc1e597 100644 --- a/src/services/i/pin.ts +++ b/src/services/i/pin.ts @@ -1,12 +1,83 @@ import config from '../../config'; import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user'; +import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user'; +import Note from '../../models/note'; 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'; +/** + * 指定した投稿をピン留めします + * @param user + * @param noteId + */ +export async function addPinned(user: IUser, noteId: mongo.ObjectID) { + // Fetch pinee + const note = await Note.findOne({ + _id: noteId, + userId: user._id + }); + + if (note === null) { + throw new Error('note not found'); + } + + const pinnedNoteIds = user.pinnedNoteIds || []; + + if (pinnedNoteIds.length > 5) { + throw new Error('cannot pin more notes'); + } + + if (pinnedNoteIds.some(id => id.equals(note._id))) { + throw new Error('already exists'); + } + + pinnedNoteIds.unshift(note._id); + + await User.update(user._id, { + $set: { + pinnedNoteIds: pinnedNoteIds + } + }); + + // Deliver to remote followers + if (isLocalUser(user)) { + deliverPinnedChange(user._id, note._id, true); + } +} + +/** + * 指定した投稿のピン留めを解除します + * @param user + * @param noteId + */ +export async function removePinned(user: IUser, noteId: mongo.ObjectID) { + // Fetch unpinee + const note = await Note.findOne({ + _id: noteId, + userId: user._id + }); + + if (note === null) { + throw new Error('note not found'); + } + + const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); + + await User.update(user._id, { + $set: { + pinnedNoteIds: pinnedNoteIds + } + }); + + // Deliver to remote followers + if (isLocalUser(user)) { + deliverPinnedChange(user._id, noteId, false); + } +} + export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) { const user = await User.findOne({ _id: userId