リモートのピン留め投稿取得対応 (#2798)

* Fetch featured

* Handle featured change

* Fix typo
This commit is contained in:
MeiMei 2018-10-02 16:27:36 +09:00 committed by syuilo
parent 15fad66fea
commit 43a5595d54
11 changed files with 199 additions and 63 deletions

View file

@ -113,6 +113,7 @@ export interface ILocalUser extends IUserBase {
export interface IRemoteUser extends IUserBase { export interface IRemoteUser extends IUserBase {
inbox: string; inbox: string;
sharedInbox?: string; sharedInbox?: string;
featured?: string;
endpoints: string[]; endpoints: string[];
uri: string; uri: string;
url?: string; url?: string;

View file

@ -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<void> => {
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}`);
};

View file

@ -8,6 +8,8 @@ import like from './like';
import announce from './announce'; import announce from './announce';
import accept from './accept'; import accept from './accept';
import reject from './reject'; import reject from './reject';
import add from './add';
import remove from './remove';
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
switch (activity.type) { switch (activity.type) {
@ -31,6 +33,14 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
await reject(actor, activity); await reject(actor, activity);
break; 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': case 'Announce':
await announce(actor, activity); await announce(actor, activity);
break; break;

View file

@ -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<void> => {
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}`);
};

View file

@ -56,7 +56,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
log(`Creating the Note: ${note.id}`); 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) { if (actor.isSuspended) {
@ -73,7 +73,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility = 'followers'; visibility = 'followers';
} else { } else {
visibility = 'specified'; 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 //#endergion

View file

@ -3,15 +3,16 @@ import { toUnicode } from 'punycode';
import * as debug from 'debug'; import * as debug from 'debug';
import config from '../../../config'; 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 Resolver from '../resolver';
import { resolveImage } from './image'; import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, IPerson } from '../type'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file'; import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta'; import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm'; import htmlToMFM from '../../../mfm/html-to-mfm';
import { updateUserStats } from '../../../services/update-chart'; import { updateUserStats } from '../../../services/update-chart';
import { URL } from 'url'; import { URL } from 'url';
import { resolveNote } from './note';
const log = debug('misskey:activitypub'); const log = debug('misskey:activitypub');
@ -155,6 +156,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
}, },
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox, sharedInbox: person.sharedInbox,
featured: person.featured,
endpoints: person.endpoints, endpoints: person.endpoints,
uri: person.id, uri: person.id,
url: person.url, url: person.url,
@ -211,6 +213,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
user.bannerUrl = bannerUrl; user.bannerUrl = bannerUrl;
//#endregion //#endregion
await updateFeatured(user._id).catch(err => console.log(err));
return user; return user;
} }
@ -282,6 +285,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
updatedAt: new Date(), updatedAt: new Date(),
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox, sharedInbox: person.sharedInbox,
featured: person.featured,
avatarId: avatar ? avatar._id : null, avatarId: avatar ? avatar._id : null,
bannerId: banner ? banner._id : null, bannerId: banner ? banner._id : null,
avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : 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に対象のPersonが登録されていればそれを返し
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> { export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> {
if (typeof uri !== 'string') throw 'uri is not string'; if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -323,5 +329,37 @@ export async function resolvePerson(uri: string, verifier?: string): Promise<IUs
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
return await createPerson(uri); if (resolver == null) resolver = new Resolver();
return await createPerson(uri, resolver);
}
export async function updateFeatured(userId: mongo.ObjectID) {
const user = await User.findOne({ _id: userId });
if (!isRemoteUser(user)) return;
if (!user.featured) return;
log(`Updating the featured: ${user.uri}`);
const resolver = new Resolver();
// Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
// Resolve to Object(may be Note) arrays
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
const items = await resolver.resolve(unresolvedItems);
if (!Array.isArray(items)) throw new Error(`Collection items is not an array`);
// Resolve and regist Notes
const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note')
.slice(0, 5)
.map(item => resolveNote(item, resolver)));
await User.update({ _id: user._id }, {
$set: {
pinnedNoteIds: featuredNotes.map(note => note._id)
}
});
} }

View file

@ -19,11 +19,11 @@ export default class Resolver {
switch (collection.type) { switch (collection.type) {
case 'Collection': case 'Collection':
collection.objects = collection.object.items; collection.objects = collection.items;
break; break;
case 'OrderedCollection': case 'OrderedCollection':
collection.objects = collection.object.orderedItems; collection.objects = collection.orderedItems;
break; break;
default: default:

View file

@ -91,6 +91,14 @@ export interface IReject extends IActivity {
type: 'Reject'; type: 'Reject';
} }
export interface IAdd extends IActivity {
type: 'Add';
}
export interface IRemove extends IActivity {
type: 'Remove';
}
export interface ILike extends IActivity { export interface ILike extends IActivity {
type: 'Like'; type: 'Like';
_misskey_reaction: string; _misskey_reaction: string;
@ -109,5 +117,7 @@ export type Object =
IFollow | IFollow |
IAccept | IAccept |
IReject | IReject |
IAdd |
IRemove |
ILike | ILike |
IAnnounce; IAnnounce;

View file

@ -1,8 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } 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'; import getParams from '../../get-params';
export const meta = { export const meta = {
@ -27,41 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
const [ps, psErr] = getParams(meta, params); const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr); if (psErr) return rej(psErr);
// Fetch pinee // Processing
const note = await Note.findOne({ try {
_id: ps.noteId, await addPinned(user, ps.noteId);
userId: user._id } catch (e) {
}); return rej(e.message);
if (note === null) {
return rej('note not found');
} }
const pinnedNoteIds = user.pinnedNoteIds || []; // Serialize
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
}
});
const iObj = await pack(user, user, { const iObj = await pack(user, user, {
detail: true detail: true
}); });
// Send response // Send response
res(iObj); res(iObj);
// Send Add to followers
deliverPinnedChange(user._id, note._id, true);
}); });

View file

@ -1,8 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } 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'; import getParams from '../../get-params';
export const meta = { export const meta = {
@ -27,31 +26,18 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
const [ps, psErr] = getParams(meta, params); const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr); if (psErr) return rej(psErr);
// Fetch unpinee // Processing
const note = await Note.findOne({ try {
_id: ps.noteId, await removePinned(user, ps.noteId);
userId: user._id } catch (e) {
}); return rej(e.message);
if (note === null) {
return rej('note not found');
} }
const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); // Serialize
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
const iObj = await pack(user, user, { const iObj = await pack(user, user, {
detail: true detail: true
}); });
// Send response // Send response
res(iObj); res(iObj);
// Send Remove to followers
deliverPinnedChange(user._id, note._id, false);
}); });

View file

@ -1,12 +1,83 @@
import config from '../../config'; import config from '../../config';
import * as mongo from 'mongodb'; 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 Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add'; import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove'; import renderRemove from '../../remote/activitypub/renderer/remove';
import packAp from '../../remote/activitypub/renderer'; import packAp from '../../remote/activitypub/renderer';
import { deliver } from '../../queue'; 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) { export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
const user = await User.findOne({ const user = await User.findOne({
_id: userId _id: userId