Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Likes collection #3159

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/remote/activitypub/renderer/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export default async function renderNote(note: INote, dive = true, isTalk = fals
inReplyTo,
attachment: files.map(renderDocument),
sensitive: note.cw != null || files.some(file => file.metadata.isSensitive),
likes: `${config.url}/notes/${note._id}/likes`,
tag,
...asPoll,
...asTalk
Expand Down
5 changes: 4 additions & 1 deletion src/server/activitypub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import renderKey from '../remote/activitypub/renderer/key';
import renderPerson from '../remote/activitypub/renderer/person';
import renderEmoji from '../remote/activitypub/renderer/emoji';
import Outbox, { packActivity } from './activitypub/outbox';
import Likes from './activitypub/likes';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
import Featured from './activitypub/featured';
Expand Down Expand Up @@ -71,7 +72,7 @@ export function setResponseType(ctx: Router.RouterContext) {
router.post('/inbox', json() as any, inbox);
router.post('/users/:user/inbox', json() as any, inbox);

const isNoteUserAvailable = async (note: INote) => {
export const isNoteUserAvailable = async (note: INote) => {
const user = await User.findOne({
_id: note.userId,
isDeleted: { $ne: true },
Expand Down Expand Up @@ -148,6 +149,8 @@ router.get('/notes/:note/activity', async ctx => {
setResponseType(ctx);
});

router.get('/notes/:note/likes', Likes);

// outbox
router.get('/users/:user/outbox', Outbox);

Expand Down
101 changes: 101 additions & 0 deletions src/server/activitypub/likes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ObjectID } from 'mongodb';
import * as Router from '@koa/router';
import config from '../../config';
import $ from 'cafy';
import ID, { transform } from '../../misc/cafy-id';
import { renderLike } from '../../remote/activitypub/renderer/like';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import { setResponseType, isNoteUserAvailable } from '../activitypub';

import Note from '../../models/note';
import { sum } from '../../prelude/array';
import * as url from '../../prelude/url';
import NoteReaction from '../../models/note-reaction';

export default async (ctx: Router.RouterContext) => {
if (config.disableFederation) ctx.throw(404);

if (!ObjectID.isValid(ctx.params.note)) {
ctx.status = 404;
return;
}

const note = await Note.findOne({
_id: new ObjectID(ctx.params.note),
deletedAt: { $exists: false },
'_user.host': null,
visibility: { $in: ['public', 'home'] },
localOnly: { $ne: true },
copyOnce: { $ne: true }
});

if (note == null || !await isNoteUserAvailable(note)) {
ctx.status = 404;
return;
}

// Get 'cursor' parameter
const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor);

// Get 'page' parameter
const pageErr = !$.optional.str.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;
}

const limit = 10;
const partOf = `${config.url}/notes/${note._id}/likes`;

if (page) {
const query = {
noteId: note._id
} as any;

// カーソルが指定されている場合
if (cursor) {
query._id = {
$lt: transform(cursor)
};
}

const reactions = await NoteReaction.find(query, {
limit: limit + 1,
sort: { _id: -1 },
// TODO: local only
});

// 「次のページ」があるかどうか
const inStock = reactions.length === limit + 1;
if (inStock) reactions.pop();

const renderedLikes = await Promise.all(reactions.map(reaction => renderLike(reaction, note)));
const rendered = renderOrderedCollectionPage(
`${partOf}?${url.query({
page: 'true',
cursor
})}`,
sum(Object.values(note.reactionCounts)),
renderedLikes, partOf,
null,
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: reactions[reactions.length - 1]._id.toHexString()
})}` : null
);

ctx.body = renderActivity(rendered);
setResponseType(ctx);
} else {
// index page
const rendered = renderOrderedCollection(partOf, sum(Object.values(note.reactionCounts)), `${partOf}?page=true`, null);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=30');
setResponseType(ctx);
}
};