mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-23 00:29:22 +09:00
Merge 9a7162a199
into e8bf6285cb
This commit is contained in:
commit
dda7928f22
@ -1,7 +1,7 @@
|
||||
## 2024.11.1
|
||||
|
||||
### General
|
||||
-
|
||||
- Enhance: リアクションの一覧の公開設定が連合されるように
|
||||
|
||||
### Client
|
||||
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
|
||||
|
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RemotePublicReactionsSetFalse1723208290742 {
|
||||
name = 'RemotePublicReactionsSetFalse1723208290742'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`UPDATE "user_profile" SET "publicReactions" = FALSE WHERE "userHost" IS NOT NULL`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// no valid down migration
|
||||
}
|
||||
}
|
@ -472,6 +472,13 @@ export class ApRendererService {
|
||||
|
||||
const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
|
||||
|
||||
const likedId = `${id}/liked`;
|
||||
const liked = this.renderOrderedCollection(
|
||||
likedId,
|
||||
undefined,
|
||||
profile.publicReactions ? `${likedId}?page=true` : undefined,
|
||||
);
|
||||
|
||||
const tag = [
|
||||
...apemojis,
|
||||
...hashtagTags,
|
||||
@ -486,6 +493,7 @@ export class ApRendererService {
|
||||
outbox: `${id}/outbox`,
|
||||
followers: `${id}/followers`,
|
||||
following: `${id}/following`,
|
||||
liked,
|
||||
featured: `${id}/collections/featured`,
|
||||
sharedInbox: `${this.config.url}/inbox`,
|
||||
endpoints: { sharedInbox: `${this.config.url}/inbox` },
|
||||
@ -670,7 +678,7 @@ export class ApRendererService {
|
||||
* @param orderedItems attached objects (optional)
|
||||
*/
|
||||
@bindThis
|
||||
public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
|
||||
public renderOrderedCollection(id: string, totalItems?: number, first?: string, last?: string, orderedItems?: IObject[]) {
|
||||
const page: any = {
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
|
@ -321,17 +321,17 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const isBot = getApType(object) === 'Service' || getApType(object) === 'Application';
|
||||
|
||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
||||
const [publicReactions, followingIsPublic, followersIsPublic] = await Promise.all(
|
||||
[
|
||||
this.isPublicCollection(person.liked, resolver),
|
||||
this.isPublicCollection(person.following, resolver),
|
||||
this.isPublicCollection(person.followers, resolver),
|
||||
].map((p): Promise<'public' | 'private'> => p
|
||||
.then(isPublic => isPublic ? 'public' : 'private')
|
||||
].map((p): Promise<boolean> => p
|
||||
.catch(err => {
|
||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
this.logger.error('error occurred while fetching actor collection', { stack: err });
|
||||
}
|
||||
return 'private';
|
||||
return false;
|
||||
}),
|
||||
),
|
||||
);
|
||||
@ -411,8 +411,9 @@ export class ApPersonService implements OnModuleInit {
|
||||
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||
url,
|
||||
fields,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
publicReactions,
|
||||
followingVisibility: followingIsPublic ? 'public' : 'private',
|
||||
followersVisibility: followersIsPublic ? 'public' : 'private',
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
userHost: host,
|
||||
@ -522,19 +523,19 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||
|
||||
const [followingVisibility, followersVisibility] = await Promise.all(
|
||||
const [publicReactions, followingIsPublic, followersIsPublic] = await Promise.all(
|
||||
[
|
||||
this.isPublicCollection(person.liked, resolver),
|
||||
this.isPublicCollection(person.following, resolver),
|
||||
this.isPublicCollection(person.followers, resolver),
|
||||
].map((p): Promise<'public' | 'private' | undefined> => p
|
||||
.then(isPublic => isPublic ? 'public' : 'private')
|
||||
].map((p): Promise<boolean | undefined> => p
|
||||
.catch(err => {
|
||||
if (!(err instanceof StatusError) || err.isRetryable) {
|
||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||
this.logger.error('error occurred while fetching actor collection', { stack: err });
|
||||
// Do not update the visibiility on transient errors.
|
||||
return undefined;
|
||||
}
|
||||
return 'private';
|
||||
return false;
|
||||
}),
|
||||
),
|
||||
);
|
||||
@ -618,8 +619,9 @@ export class ApPersonService implements OnModuleInit {
|
||||
fields,
|
||||
description: _description,
|
||||
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||
followingVisibility,
|
||||
followersVisibility,
|
||||
publicReactions,
|
||||
followingVisibility: followingIsPublic ? 'public' : followingIsPublic === false ? 'private' : undefined,
|
||||
followersVisibility: followersIsPublic ? 'public' : followersIsPublic === false ? 'private' : undefined,
|
||||
birthday: bday?.[0] ?? null,
|
||||
location: person['vcard:Address'] ?? null,
|
||||
});
|
||||
|
@ -103,14 +103,14 @@ export interface IActivity extends IObject {
|
||||
|
||||
export interface ICollection extends IObject {
|
||||
type: 'Collection';
|
||||
totalItems: number;
|
||||
totalItems?: number;
|
||||
first?: IObject | string;
|
||||
items?: ApObject;
|
||||
}
|
||||
|
||||
export interface IOrderedCollection extends IObject {
|
||||
type: 'OrderedCollection';
|
||||
totalItems: number;
|
||||
totalItems?: number;
|
||||
first?: IObject | string;
|
||||
orderedItems?: ApObject;
|
||||
}
|
||||
@ -190,6 +190,7 @@ export interface IActor extends IObject {
|
||||
following?: string | ICollection | IOrderedCollection;
|
||||
featured?: string | IOrderedCollection;
|
||||
outbox: string | IOrderedCollection;
|
||||
liked?: string | ICollection | IOrderedCollection;
|
||||
endpoints?: {
|
||||
sharedInbox?: string;
|
||||
};
|
||||
|
@ -545,7 +545,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
}),
|
||||
pinnedPageId: profile!.pinnedPageId,
|
||||
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
|
||||
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
||||
publicReactions: profile!.publicReactions,
|
||||
followersVisibility: profile!.followersVisibility,
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||
|
@ -352,6 +352,98 @@ export class ActivityPubServerService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async liked(
|
||||
request: FastifyRequest<{ Params: { user: string; }; Querystring: { cursor?: string; page?: string; }; }>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const userId = request.params.user;
|
||||
|
||||
const cursor = request.query.cursor;
|
||||
if (cursor != null && typeof cursor !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const page = request.query.page === 'true';
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
id: userId,
|
||||
host: IsNull(),
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
|
||||
if (!profile.publicReactions) {
|
||||
reply.code(403);
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = 10;
|
||||
const partOf = `${this.config.url}/users/${userId}/liked`;
|
||||
|
||||
if (page) {
|
||||
const query = this.noteReactionsRepository.createQueryBuilder('reaction')
|
||||
.andWhere('reaction.userId = :userId', { userId: user.id });
|
||||
|
||||
// カーソルが指定されている場合
|
||||
if (cursor) {
|
||||
query.andWhere('reaction.id < :id', { id: cursor });
|
||||
}
|
||||
|
||||
const reactions = await query
|
||||
.limit(limit + 1)
|
||||
.orderBy('reaction.id', 'DESC')
|
||||
.innerJoinAndSelect('reaction.note', 'note')
|
||||
.leftJoinAndSelect('note.user', 'noteUser')
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE')
|
||||
.andWhere('noteUser.isSuspended = FALSE')
|
||||
.getMany();
|
||||
|
||||
// 「次のページ」があるかどうか
|
||||
const inStock = reactions.length === limit + 1;
|
||||
if (inStock) reactions.pop();
|
||||
|
||||
const reactedNotes = await Promise.all(reactions.map(({ note }) => note!.uri || this.apRendererService.renderNote(note!, false)));
|
||||
const rendered = this.apRendererService.renderOrderedCollectionPage(
|
||||
`${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor,
|
||||
})}`,
|
||||
undefined, reactedNotes, partOf,
|
||||
undefined,
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: reactions.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
undefined,
|
||||
`${partOf}?page=true`,
|
||||
);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async featured(request: FastifyRequest<{ Params: { user: string; }; }>, reply: FastifyReply) {
|
||||
const userId = request.params.user;
|
||||
@ -629,6 +721,12 @@ export class ActivityPubServerService {
|
||||
Querystring: { cursor?: string; page?: string; };
|
||||
}>('/users/:user/following', async (request, reply) => await this.following(request, reply));
|
||||
|
||||
// liked
|
||||
fastify.get<{
|
||||
Params: { user: string; };
|
||||
Querystring: { cursor?: string; page?: string; };
|
||||
}>('/users/:user/liked', async (request, reply) => await this.liked(request, reply));
|
||||
|
||||
// featured
|
||||
fastify.get<{ Params: { user: string; }; }>('/users/:user/collections/featured', async (request, reply) => await this.featured(request, reply));
|
||||
|
||||
|
@ -10,7 +10,6 @@ import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
@ -38,11 +37,6 @@ export const meta = {
|
||||
code: 'REACTIONS_NOT_PUBLIC',
|
||||
id: '673a7dd2-6924-1093-e0c0-e68456ceae5c',
|
||||
},
|
||||
isRemoteUser: {
|
||||
message: 'Currently unavailable to display reactions of remote users. See https://github.com/misskey-dev/misskey/issues/12964',
|
||||
code: 'IS_REMOTE_USER',
|
||||
id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@ -69,7 +63,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
private noteReactionsRepository: NoteReactionsRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteReactionEntityService: NoteReactionEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
@ -78,11 +71,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const userIdsWhoBlockingMe = me ? await this.cacheService.userBlockedCache.fetch(me.id) : new Set<string>();
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
|
||||
if (!iAmModerator) {
|
||||
const user = await this.cacheService.findUserById(ps.userId);
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
throw new ApiError(meta.errors.isRemoteUser);
|
||||
}
|
||||
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
|
||||
if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
|
||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
||||
|
@ -43,12 +43,11 @@ describe('User', () => {
|
||||
'uri',
|
||||
'createdAt',
|
||||
'lastFetchedAt',
|
||||
'publicReactions',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ffVisibility is federated', () => {
|
||||
describe('ff/reactions visibility is federated', () => {
|
||||
let alice: LoginUser, bob: LoginUser;
|
||||
let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
|
||||
|
||||
@ -78,6 +77,7 @@ describe('User', () => {
|
||||
])) {
|
||||
strictEqual(user.followersVisibility, 'public');
|
||||
strictEqual(user.followingVisibility, 'public');
|
||||
strictEqual(user.publicReactions, true);
|
||||
}
|
||||
});
|
||||
|
||||
@ -113,6 +113,22 @@ describe('User', () => {
|
||||
strictEqual(user.followingVisibility, 'private');
|
||||
}
|
||||
});
|
||||
|
||||
test.skip('Setting false for publicReactions is federated', async () => {
|
||||
await Promise.all([
|
||||
alice.client.request('i/update', { publicReactions: false }),
|
||||
bob.client.request('i/update', { publicReactions: false }),
|
||||
]);
|
||||
await sleep();
|
||||
|
||||
for (const user of await Promise.all([
|
||||
alice.client.request('users/show', { userId: bobInA.id }),
|
||||
bob.client.request('users/show', { userId: aliceInB.id }),
|
||||
])) {
|
||||
strictEqual(user.publicReactions, false);
|
||||
strictEqual(user.publicReactions, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCat is federated', () => {
|
||||
|
@ -213,7 +213,7 @@ describe('ActivityPub', () => {
|
||||
});
|
||||
|
||||
describe('Collection visibility', () => {
|
||||
test('Public following/followers', async () => {
|
||||
test('Public following/followers/reactions', async () => {
|
||||
const actor = createRandomActor();
|
||||
actor.following = {
|
||||
id: `${actor.id}/following`,
|
||||
@ -222,6 +222,12 @@ describe('ActivityPub', () => {
|
||||
first: `${actor.id}/following?page=1`,
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
actor.liked = {
|
||||
id: `${actor.id}/following`,
|
||||
type: 'OrderedCollection',
|
||||
totalItems: 0,
|
||||
orderedItems: [],
|
||||
};
|
||||
|
||||
resolver.register(actor.id, actor);
|
||||
resolver.register(actor.followers, {
|
||||
@ -236,9 +242,10 @@ describe('ActivityPub', () => {
|
||||
|
||||
assert.deepStrictEqual(userProfile.followingVisibility, 'public');
|
||||
assert.deepStrictEqual(userProfile.followersVisibility, 'public');
|
||||
assert.deepStrictEqual(userProfile.publicReactions, true);
|
||||
});
|
||||
|
||||
test('Private following/followers', async () => {
|
||||
test('Private following/followers/reactions', async () => {
|
||||
const actor = createRandomActor();
|
||||
actor.following = {
|
||||
id: `${actor.id}/following`,
|
||||
@ -247,6 +254,7 @@ describe('ActivityPub', () => {
|
||||
// first: …
|
||||
};
|
||||
actor.followers = `${actor.id}/followers`;
|
||||
// actor.liked = …;
|
||||
|
||||
resolver.register(actor.id, actor);
|
||||
//resolver.register(actor.followers, { … });
|
||||
@ -256,6 +264,7 @@ describe('ActivityPub', () => {
|
||||
|
||||
assert.deepStrictEqual(userProfile.followingVisibility, 'private');
|
||||
assert.deepStrictEqual(userProfile.followersVisibility, 'private');
|
||||
assert.deepStrictEqual(userProfile.publicReactions, false);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user