Publish pinned notes (#2731)
This commit is contained in:
		
							parent
							
								
									bec48319ec
								
							
						
					
					
						commit
						11496d887e
					
				
					 9 changed files with 135 additions and 2 deletions
				
			
		
							
								
								
									
										9
									
								
								src/remote/activitypub/renderer/add.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/remote/activitypub/renderer/add.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -4,8 +4,9 @@
 | 
				
			||||||
 * @param totalItems Total number of items
 | 
					 * @param totalItems Total number of items
 | 
				
			||||||
 * @param first URL of first page (optional)
 | 
					 * @param first URL of first page (optional)
 | 
				
			||||||
 * @param last URL of last 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 = {
 | 
						const page: any = {
 | 
				
			||||||
		id,
 | 
							id,
 | 
				
			||||||
		type: 'OrderedCollection',
 | 
							type: 'OrderedCollection',
 | 
				
			||||||
| 
						 | 
					@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (first) page.first = first;
 | 
						if (first) page.first = first;
 | 
				
			||||||
	if (last) page.last = last;
 | 
						if (last) page.last = last;
 | 
				
			||||||
 | 
						if (orderedItems) page.orderedItems = orderedItems;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return page;
 | 
						return page;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ export default async (user: ILocalUser) => {
 | 
				
			||||||
		outbox: `${id}/outbox`,
 | 
							outbox: `${id}/outbox`,
 | 
				
			||||||
		followers: `${id}/followers`,
 | 
							followers: `${id}/followers`,
 | 
				
			||||||
		following: `${id}/following`,
 | 
							following: `${id}/following`,
 | 
				
			||||||
 | 
							featured: `${id}/collections/featured`,
 | 
				
			||||||
		sharedInbox: `${config.url}/inbox`,
 | 
							sharedInbox: `${config.url}/inbox`,
 | 
				
			||||||
		url: `${config.url}/@${user.username}`,
 | 
							url: `${config.url}/@${user.username}`,
 | 
				
			||||||
		preferredUsername: user.username,
 | 
							preferredUsername: user.username,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/remote/activitypub/renderer/remove.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/remote/activitypub/renderer/remove.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -53,6 +53,7 @@ export interface IPerson extends IObject {
 | 
				
			||||||
	publicKey: any;
 | 
						publicKey: any;
 | 
				
			||||||
	followers: any;
 | 
						followers: any;
 | 
				
			||||||
	following: any;
 | 
						following: any;
 | 
				
			||||||
 | 
						featured?: any;
 | 
				
			||||||
	outbox: any;
 | 
						outbox: any;
 | 
				
			||||||
	endpoints: string[];
 | 
						endpoints: string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person';
 | 
				
			||||||
import Outbox, { packActivity } from './activitypub/outbox';
 | 
					import Outbox, { packActivity } from './activitypub/outbox';
 | 
				
			||||||
import Followers from './activitypub/followers';
 | 
					import Followers from './activitypub/followers';
 | 
				
			||||||
import Following from './activitypub/following';
 | 
					import Following from './activitypub/following';
 | 
				
			||||||
 | 
					import Featured from './activitypub/featured';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Init router
 | 
					// Init router
 | 
				
			||||||
const router = new Router();
 | 
					const router = new Router();
 | 
				
			||||||
| 
						 | 
					@ -102,6 +103,9 @@ router.get('/users/:user/followers', Followers);
 | 
				
			||||||
// following
 | 
					// following
 | 
				
			||||||
router.get('/users/:user/following', Following);
 | 
					router.get('/users/:user/following', Following);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// featured
 | 
				
			||||||
 | 
					router.get('/users/:user/collections/featured', Featured);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// publickey
 | 
					// publickey
 | 
				
			||||||
router.get('/users/:user/publickey', async ctx => {
 | 
					router.get('/users/:user/publickey', async ctx => {
 | 
				
			||||||
	const userId = new mongo.ObjectID(ctx.params.user);
 | 
						const userId = new mongo.ObjectID(ctx.params.user);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										38
									
								
								src/server/activitypub/featured.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/server/activitypub/featured.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
 | 
				
			||||||
import User, { ILocalUser } from '../../../../models/user';
 | 
					import User, { ILocalUser } from '../../../../models/user';
 | 
				
			||||||
import Note from '../../../../models/note';
 | 
					import Note from '../../../../models/note';
 | 
				
			||||||
import { pack } from '../../../../models/user';
 | 
					import { pack } from '../../../../models/user';
 | 
				
			||||||
 | 
					import { deliverPinnedChange } from '../../../../services/i/pin';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Pin note
 | 
					 * Pin note
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 | 
				
			||||||
		return rej('note not found');
 | 
							return rej('note not found');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let addedId;
 | 
				
			||||||
 | 
						let removedId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const pinnedNoteIds = user.pinnedNoteIds || [];
 | 
						const pinnedNoteIds = user.pinnedNoteIds || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (pinnedNoteIds.some(id => id.equals(note._id))) {
 | 
						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);
 | 
						pinnedNoteIds.unshift(note._id);
 | 
				
			||||||
 | 
						addedId = note._id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (pinnedNoteIds.length > 5) {
 | 
						if (pinnedNoteIds.length > 5) {
 | 
				
			||||||
		pinnedNoteIds.pop();
 | 
							removedId = pinnedNoteIds.pop();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await User.update(user._id, {
 | 
						await User.update(user._id, {
 | 
				
			||||||
| 
						 | 
					@ -44,6 +49,9 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
 | 
				
			||||||
		detail: true
 | 
							detail: true
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Send Add/Remove to followers
 | 
				
			||||||
 | 
						deliverPinnedChange(user._id, removedId, addedId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Send response
 | 
						// Send response
 | 
				
			||||||
	res(iObj);
 | 
						res(iObj);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										61
									
								
								src/services/i/pin.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/services/i/pin.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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<string[]> {
 | 
				
			||||||
 | 
						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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue