Skip to content

Commit

Permalink
Likes collection 2024 (#4777)
Browse files Browse the repository at this point in the history
* Likes collection 2024

* APでリモートの /users/:user はリダイレクト
  • Loading branch information
mei23 authored Jan 16, 2024
1 parent ae07402 commit 6817968
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 5 deletions.
2 changes: 2 additions & 0 deletions src/models/note-reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export default NoteReaction;

export interface INoteReaction {
_id: mongo.ObjectID;
/** AP id (remote only) */
uri?: string;
createdAt: Date;
noteId: mongo.ObjectID;
userId: mongo.ObjectID;
Expand Down
2 changes: 1 addition & 1 deletion src/remote/activitypub/kernel/like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default async (actor: IRemoteUser, activity: ILike): Promise<string> => {
await extractEmojis(activity.tag, actor.host).catch(() => null);

try {
await create(actor, note, activity._misskey_reaction || activity.content || activity.name, getApType(activity) === 'Dislike');
await create(actor, note, activity._misskey_reaction || activity.content || activity.name, getApType(activity) === 'Dislike', getApId(activity));
} catch (e: any) {
if (e instanceof ReactionError) {
return `skip: ${e.type}`;
Expand Down
1 change: 1 addition & 0 deletions src/remote/activitypub/renderer/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
inReplyTo,
attachment: files.map(renderDocument),
sensitive: note.cw != null || files.some(file => file.metadata.isSensitive),
likes: `${config.url}/notes/${note._id}/likes`,
tag,
...asPoll,
};
Expand Down
14 changes: 11 additions & 3 deletions src/server/activitypub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import * as httpSignature from '@peertube/http-signature';

import { renderActivity } from '../remote/activitypub/renderer';
import Note, { INote } from '../models/note';
import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
import User, { isLocalUser, ILocalUser, IUser, isRemoteUser } from '../models/user';
import Emoji from '../models/emoji';
import renderNote from '../remote/activitypub/renderer/note';
import renderKey from '../remote/activitypub/renderer/key';
import renderPerson from '../remote/activitypub/renderer/person';
import renderEmoji from '../remote/activitypub/renderer/emoji';
import Likes from './activitypub/likes';
import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
Expand Down Expand Up @@ -239,7 +240,7 @@ export function setResponseType(ctx: Router.RouterContext) {
router.post('/inbox', inbox);
router.post('/users/:user/inbox', 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 @@ -330,6 +331,9 @@ router.get('/notes/:note/activity', async ctx => {
setResponseType(ctx);
});

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

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

Expand Down Expand Up @@ -404,9 +408,13 @@ router.get('/users/:user', async (ctx, next) => {
isDeleted: { $ne: true },
isSuspended: { $ne: true },
noFederation: { $ne: true },
host: null
});

if (isRemoteUser(user)) {
ctx.redirect(user.uri);
return;
}

await userInfo(ctx, user);
});

Expand Down
102 changes: 102 additions & 0 deletions src/server/activitypub/likes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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 = 100;
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 },
});

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

const renderedLikes = await Promise.all(reactions.map(reaction => reaction.uri ?? 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);
ctx.set('Cache-Control', 'public, max-age=180');
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=180');
setResponseType(ctx);
}
};
3 changes: 2 additions & 1 deletion src/services/note/reaction/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class ReactionError extends Error {
}
//#endregion Error

export default async (user: IUser, note: INote, reaction?: string, dislike = false): Promise<INoteReaction> => {
export default async (user: IUser, note: INote, reaction?: string, dislike = false, uri?: string): Promise<INoteReaction> => {
// detect direction
// LL => local to local, LR => local to remote, RL => remote to local, RR => remote to remote
const direction = `${ isLocalUser(user) ? 'L' : 'R' }${ note._user.host == null ? 'L' : 'R' }`;
Expand All @@ -47,6 +47,7 @@ export default async (user: IUser, note: INote, reaction?: string, dislike = fal

const inserted = {
_id: new mongo.ObjectID(),
uri,
createdAt: new Date(),
noteId: note._id,
userId: user._id,
Expand Down

0 comments on commit 6817968

Please sign in to comment.