Merge pull request #2210 from mei23/mei-0814-ap3
ActivityPub Followers/Following/Outbox の実装
This commit is contained in:
		
						commit
						fada899b30
					
				|  | @ -0,0 +1,16 @@ | |||
| import config from '../../../config'; | ||||
| import * as mongo from 'mongodb'; | ||||
| import User, { isLocalUser } from '../../../models/user'; | ||||
| 
 | ||||
| /** | ||||
|  * Convert (local|remote)(Follower|Followee)ID to URL | ||||
|  * @param id Follower|Followee ID | ||||
|  */ | ||||
| export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> { | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: id | ||||
| 	}); | ||||
| 
 | ||||
| 	return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri; | ||||
| } | ||||
|  | @ -0,0 +1,23 @@ | |||
| /** | ||||
|  * Render OrderedCollectionPage | ||||
|  * @param id URL of self | ||||
|  * @param totalItems Number of total items | ||||
|  * @param orderedItems Items | ||||
|  * @param partOf URL of base | ||||
|  * @param prev URL of prev page (optional) | ||||
|  * @param next URL of next page (optional) | ||||
|  */ | ||||
| export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev: string, next: string) { | ||||
| 	const page = { | ||||
| 		id, | ||||
| 		partOf, | ||||
| 		type: 'OrderedCollectionPage', | ||||
| 		totalItems, | ||||
| 		orderedItems | ||||
| 	} as any; | ||||
| 
 | ||||
| 	if (prev) page.prev = prev; | ||||
| 	if (next) page.next = next; | ||||
| 
 | ||||
| 	return page; | ||||
| } | ||||
|  | @ -1,6 +1,19 @@ | |||
| export default (id: string, totalItems: any, orderedItems: any) => ({ | ||||
| 	id, | ||||
| 	type: 'OrderedCollection', | ||||
| 	totalItems, | ||||
| 	orderedItems | ||||
| }); | ||||
| /** | ||||
|  * Render OrderedCollection | ||||
|  * @param id URL of self | ||||
|  * @param totalItems Total number of items | ||||
|  * @param first URL of first page (optional) | ||||
|  * @param last URL of last page (optional) | ||||
|  */ | ||||
| export default function(id: string, totalItems: any, first: string, last: string) { | ||||
| 	const page: any = { | ||||
| 		id, | ||||
| 		type: 'OrderedCollection', | ||||
| 		totalItems, | ||||
| 	}; | ||||
| 
 | ||||
| 	if (first) page.first = first; | ||||
| 	if (last) page.last = last; | ||||
| 
 | ||||
| 	return page; | ||||
| } | ||||
|  |  | |||
|  | @ -10,8 +10,9 @@ import User, { isLocalUser, ILocalUser, IUser } from '../models/user'; | |||
| import renderNote from '../remote/activitypub/renderer/note'; | ||||
| import renderKey from '../remote/activitypub/renderer/key'; | ||||
| import renderPerson from '../remote/activitypub/renderer/person'; | ||||
| import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection'; | ||||
| import config from '../config'; | ||||
| import Outbox from './activitypub/outbox'; | ||||
| import Followers from './activitypub/followers'; | ||||
| import Following from './activitypub/following'; | ||||
| 
 | ||||
| // Init router
 | ||||
| const router = new Router(); | ||||
|  | @ -64,72 +65,14 @@ router.get('/notes/:note', async (ctx, next) => { | |||
| 	ctx.body = pack(await renderNote(note)); | ||||
| }); | ||||
| 
 | ||||
| // outbot
 | ||||
| router.get('/users/:user/outbox', async ctx => { | ||||
| 	const userId = new mongo.ObjectID(ctx.params.user); | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: userId, | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const notes = await Note.find({ userId: user._id }, { | ||||
| 		limit: 10, | ||||
| 		sort: { _id: -1 } | ||||
| 	}); | ||||
| 
 | ||||
| 	const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); | ||||
| 	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); | ||||
| 
 | ||||
| 	ctx.body = pack(rendered); | ||||
| }); | ||||
| // outbox
 | ||||
| router.get('/users/:user/outbox', Outbox); | ||||
| 
 | ||||
| // followers
 | ||||
| router.get('/users/:user/followers', async ctx => { | ||||
| 	const userId = new mongo.ObjectID(ctx.params.user); | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: userId, | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: Implement fetch and render
 | ||||
| 
 | ||||
| 	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/followers`, 0, []); | ||||
| 
 | ||||
| 	ctx.body = pack(rendered); | ||||
| }); | ||||
| router.get('/users/:user/followers', Followers); | ||||
| 
 | ||||
| // following
 | ||||
| router.get('/users/:user/following', async ctx => { | ||||
| 	const userId = new mongo.ObjectID(ctx.params.user); | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: userId, | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: Implement fetch and render
 | ||||
| 
 | ||||
| 	const rendered = renderOrderedCollection(`${config.url}/users/${userId}/following`, 0, []); | ||||
| 
 | ||||
| 	ctx.body = pack(rendered); | ||||
| }); | ||||
| router.get('/users/:user/following', Following); | ||||
| 
 | ||||
| // publickey
 | ||||
| router.get('/users/:user/publickey', async ctx => { | ||||
|  |  | |||
|  | @ -0,0 +1,80 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import * as Koa from 'koa'; | ||||
| import config from '../../config'; | ||||
| import $ from 'cafy'; import ID from '../../misc/cafy-id'; | ||||
| import User from '../../models/user'; | ||||
| import Following from '../../models/following'; | ||||
| import pack from '../../remote/activitypub/renderer'; | ||||
| import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; | ||||
| import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; | ||||
| import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; | ||||
| 
 | ||||
| export default async (ctx: Koa.Context) => { | ||||
| 	const userId = new mongo.ObjectID(ctx.params.user); | ||||
| 
 | ||||
| 	// Get 'cursor' parameter
 | ||||
| 	const [cursor = null, cursorErr] = $.type(ID).optional.get(ctx.request.query.cursor); | ||||
| 
 | ||||
| 	// Get 'page' parameter
 | ||||
| 	const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page); | ||||
| 	const page: boolean = ctx.request.query.page === 'true'; | ||||
| 
 | ||||
| 	// Validate parameters
 | ||||
| 	if (cursorErr || pageErr) { | ||||
| 		ctx.status = 400; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// Verify user
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: userId, | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const limit = 10; | ||||
| 	const partOf = `${config.url}/users/${userId}/followers`; | ||||
| 
 | ||||
| 	if (page) { | ||||
| 		// Construct query
 | ||||
| 		const query = { | ||||
| 			followeeId: user._id | ||||
| 		} as any; | ||||
| 
 | ||||
| 		// カーソルが指定されている場合
 | ||||
| 		if (cursor) { | ||||
| 			query._id = { | ||||
| 				$lt: cursor | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		// Get followers
 | ||||
| 		const followings = await Following | ||||
| 			.find(query, { | ||||
| 				limit: limit + 1, | ||||
| 				sort: { _id: -1 } | ||||
| 			}); | ||||
| 
 | ||||
| 		// 「次のページ」があるかどうか
 | ||||
| 		const inStock = followings.length === limit + 1; | ||||
| 		if (inStock) followings.pop(); | ||||
| 
 | ||||
| 		const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId))); | ||||
| 		const rendered = renderOrderedCollectionPage( | ||||
| 			`${partOf}?page=true${cursor ? `&cursor=${cursor}` : ''}`, | ||||
| 			user.followersCount, renderedFollowers, partOf, | ||||
| 			null, | ||||
| 			inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null | ||||
| 		); | ||||
| 
 | ||||
| 		ctx.body = pack(rendered); | ||||
| 	} else { | ||||
| 		// index page
 | ||||
| 		const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null); | ||||
| 		ctx.body = pack(rendered); | ||||
| 	} | ||||
| }; | ||||
|  | @ -0,0 +1,80 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import * as Koa from 'koa'; | ||||
| import config from '../../config'; | ||||
| import $ from 'cafy'; import ID from '../../misc/cafy-id'; | ||||
| import User from '../../models/user'; | ||||
| import Following from '../../models/following'; | ||||
| import pack from '../../remote/activitypub/renderer'; | ||||
| import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; | ||||
| import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; | ||||
| import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; | ||||
| 
 | ||||
| export default async (ctx: Koa.Context) => { | ||||
| 	const userId = new mongo.ObjectID(ctx.params.user); | ||||
| 
 | ||||
| 	// Get 'cursor' parameter
 | ||||
| 	const [cursor = null, cursorErr] = $.type(ID).optional.get(ctx.request.query.cursor); | ||||
| 
 | ||||
| 	// Get 'page' parameter
 | ||||
| 	const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page); | ||||
| 	const page: boolean = ctx.request.query.page === 'true'; | ||||
| 
 | ||||
| 	// Validate parameters
 | ||||
| 	if (cursorErr || pageErr) { | ||||
| 		ctx.status = 400; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// Verify user
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: userId, | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const limit = 10; | ||||
| 	const partOf = `${config.url}/users/${userId}/following`; | ||||
| 
 | ||||
| 	if (page) { | ||||
| 		// Construct query
 | ||||
| 		const query = { | ||||
| 			followerId: user._id | ||||
| 		} as any; | ||||
| 
 | ||||
| 		// カーソルが指定されている場合
 | ||||
| 		if (cursor) { | ||||
| 			query._id = { | ||||
| 				$lt: cursor | ||||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		// Get followings
 | ||||
| 		const followings = await Following | ||||
| 			.find(query, { | ||||
| 				limit: limit + 1, | ||||
| 				sort: { _id: -1 } | ||||
| 			}); | ||||
| 
 | ||||
| 		// 「次のページ」があるかどうか
 | ||||
| 		const inStock = followings.length === limit + 1; | ||||
| 		if (inStock) followings.pop(); | ||||
| 
 | ||||
| 		const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId))); | ||||
| 		const rendered = renderOrderedCollectionPage( | ||||
| 			`${partOf}?page=true${cursor ? `&cursor=${cursor}` : ''}`, | ||||
| 			user.followingCount, renderedFollowees, partOf, | ||||
| 			null, | ||||
| 			inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null | ||||
| 		); | ||||
| 
 | ||||
| 		ctx.body = pack(rendered); | ||||
| 	} else { | ||||
| 		// index page
 | ||||
| 		const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null); | ||||
| 		ctx.body = pack(rendered); | ||||
| 	} | ||||
| }; | ||||
|  | @ -0,0 +1,96 @@ | |||
| import * as mongo from 'mongodb'; | ||||
| import * as Koa from 'koa'; | ||||
| import config from '../../config'; | ||||
| import $ from 'cafy'; import ID from '../../misc/cafy-id'; | ||||
| import User from '../../models/user'; | ||||
| import pack from '../../remote/activitypub/renderer'; | ||||
| import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; | ||||
| import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; | ||||
| 
 | ||||
| import Note from '../../models/note'; | ||||
| import renderNote from '../../remote/activitypub/renderer/note'; | ||||
| 
 | ||||
| export default async (ctx: Koa.Context) => { | ||||
| 	const userId = new mongo.ObjectID(ctx.params.user); | ||||
| 
 | ||||
| 	// Get 'sinceId' parameter
 | ||||
| 	const [sinceId, sinceIdErr] = $.type(ID).optional.get(ctx.request.query.since_id); | ||||
| 
 | ||||
| 	// Get 'untilId' parameter
 | ||||
| 	const [untilId, untilIdErr] = $.type(ID).optional.get(ctx.request.query.until_id); | ||||
| 
 | ||||
| 	// Get 'page' parameter
 | ||||
| 	const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page); | ||||
| 	const page: boolean = ctx.request.query.page === 'true'; | ||||
| 
 | ||||
| 	// Validate parameters
 | ||||
| 	if (sinceIdErr || untilIdErr || pageErr || [sinceId, untilId].filter(x => x != null).length > 1) { | ||||
| 		ctx.status = 400; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	// Verify user
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: userId, | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (user === null) { | ||||
| 		ctx.status = 404; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const limit = 20; | ||||
| 	const partOf = `${config.url}/users/${userId}/outbox`; | ||||
| 
 | ||||
| 	if (page) { | ||||
| 		//#region Construct query
 | ||||
| 		const sort = { | ||||
| 			_id: -1 | ||||
| 		}; | ||||
| 
 | ||||
| 		const query = { | ||||
| 			userId: user._id, | ||||
| 			$or: [ { visibility: 'public' }, { visibility: 'home' } ], | ||||
| 			text: { $ne: null }	// exclude renote, but include quote
 | ||||
| 		} as any; | ||||
| 
 | ||||
| 		if (sinceId) { | ||||
| 			sort._id = 1; | ||||
| 			query._id = { | ||||
| 				$gt: sinceId | ||||
| 			}; | ||||
| 		} else if (untilId) { | ||||
| 			query._id = { | ||||
| 				$lt: untilId | ||||
| 			}; | ||||
| 		} | ||||
| 		//#endregion
 | ||||
| 
 | ||||
| 		// Issue query
 | ||||
| 		const notes = await Note | ||||
| 			.find(query, { | ||||
| 				limit: limit, | ||||
| 				sort: sort | ||||
| 			}); | ||||
| 
 | ||||
| 		if (sinceId) notes.reverse(); | ||||
| 
 | ||||
| 		const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); | ||||
| 		const rendered = renderOrderedCollectionPage( | ||||
| 			`${partOf}?page=true${sinceId ? `&since_id=${sinceId}` : ''}${untilId ? `&until_id=${untilId}` : ''}`, | ||||
| 			user.notesCount, renderedNotes, partOf, | ||||
| 			notes.length > 0 ? `${partOf}?page=true&since_id=${notes[0]._id}` : null, | ||||
| 			notes.length > 0 ? `${partOf}?page=true&until_id=${notes[notes.length - 1]._id}` : null | ||||
| 		); | ||||
| 
 | ||||
| 		ctx.body = pack(rendered); | ||||
| 	} else { | ||||
| 		// index page
 | ||||
| 		const rendered = renderOrderedCollection(partOf, user.notesCount, | ||||
| 			`${partOf}?page=true`, | ||||
| 			`${partOf}?page=true&since_id=000000000000000000000000` | ||||
| 		); | ||||
| 		ctx.body = pack(rendered); | ||||
| 	} | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue